diff --git a/package.json b/package.json index e7e9983f..a6f1f3de 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,9 @@ "version": "6.10.5", "description": "Export design tokens from Figma", "main": "plugin.js", + "engines": { + "node": "~18.20.3" + }, "repository": { "type": "git", "url": "https://github.com/lukasoppermann/design-tokens.git" diff --git a/src/config/config.ts b/src/config/config.ts index 70a92a24..1cd66bcf 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -25,6 +25,7 @@ export default { authType: { token: 'token', gitlabToken: 'gitlab_token', + gitlabCommit: 'gitlab_commit', basic: 'Basic', bearer: 'Bearer' } diff --git a/src/ui/components/UrlExportSettings.tsx b/src/ui/components/UrlExportSettings.tsx index 4f1dcb5c..b33f86a0 100644 --- a/src/ui/components/UrlExportSettings.tsx +++ b/src/ui/components/UrlExportSettings.tsx @@ -111,7 +111,7 @@ export const UrlExportSettings = () => { type='text' required pattern='^https://.*' - placeholder='https://api.github.com/repos/:username/:repo/dispatches' + placeholder= {(settings.authType === config.key.authType.gitlabCommit && 'https://gitlab.com/api/v4/projects/:projectId')|| 'https://api.github.com/repos/:username/:repo/dispatches'} value={settings.serverUrl} onChange={value => updateSettings(draft => { draft.serverUrl = value })} /> @@ -156,6 +156,10 @@ export const UrlExportSettings = () => { label: '(Gitlab) token', value: config.key.authType.gitlabToken }, + { + label: '(Gitlab) Project Token', + value: config.key.authType.gitlabCommit + }, { label: 'Basic authentication', value: config.key.authType.basic @@ -198,6 +202,25 @@ export const UrlExportSettings = () => { } + {config.key.authType.gitlabCommit === settings.authType && + <> +

Branch +

+ + updateSettings(draft => { draft.reference = value })} + /> + + } + About This Export

Commit Message

diff --git a/src/ui/modules/gitlabRepository.ts b/src/ui/modules/gitlabRepository.ts new file mode 100644 index 00000000..5fbce131 --- /dev/null +++ b/src/ui/modules/gitlabRepository.ts @@ -0,0 +1,122 @@ +import { utf8ToBase64 } from "@src/utilities/base64"; +import { + urlExportRequestBody, + urlExportSettings, +} from "@typings/urlExportData"; + +export class GitlabRepository { + baseUrl: string; + token: string; + + constructor(props: { baseUrl: string; token: string }) { + this.baseUrl = props.baseUrl; + this.token = props.token; + } + + async upload( + { client_payload: clientPayload }: urlExportRequestBody, + { reference: branch }: urlExportSettings, + responseHandler: { + onError: () => void; + onLoaded: (request: XMLHttpRequest) => void; + } + ) { + const encodedContent = utf8ToBase64(clientPayload.tokens); + const encodedFilepath = encodeURIComponent(clientPayload.filename); + + let isFileExist: boolean; + try { + isFileExist = await this._checkFile(encodedFilepath, branch); + } catch (error) { + if (error && error.request && error.code === 401) { + responseHandler.onLoaded(error.request); + } + return; + } + + const uploadRequest = new XMLHttpRequest(); + uploadRequest.onerror = (_err) => responseHandler.onError(); + uploadRequest.onload = (event) => + responseHandler.onLoaded(event.target as XMLHttpRequest); + + this._uploadFile({ + request: uploadRequest, + content: encodedContent, + commitMessage: clientPayload.commitMessage, + filepath: encodedFilepath, + branch: branch, + isFileExist: isFileExist, + }); + } + + private _checkFile( + encodedFilepath: string, + branch: string + ): Promise { + return new Promise((resolve, reject) => { + const request = new XMLHttpRequest(); + request.open( + "GET", + `${this.baseUrl}/repository/files/${encodedFilepath}?ref=${branch}` + ); + this._setRequestHeader(request); + + request.onreadystatechange = (_ev: ProgressEvent) => { + if (request.readyState !== XMLHttpRequest.DONE) { + return; + } + + const statusCode = request.status; + if (statusCode === 200) { + resolve(true); + return; + } + + if (statusCode === 404) { + resolve(false); + return; + } + + reject({ + code: statusCode, + message: request.response, + request: request, + }); + }; + + request.send(); + }); + } + + private _uploadFile(args: { + request: XMLHttpRequest; + filepath: string; + content: string; + commitMessage: string; + branch: string; + isFileExist: boolean; + }) { + const { isFileExist, request, branch, content, commitMessage, filepath } = args; + + const body = { + branch: branch, + content: content, + commit_message: commitMessage || `Design token update at ${Date.now()}`, + encoding: "base64", + }; + const encodedFilepath = encodeURIComponent(filepath); + + request.open( + isFileExist ? "PUT" : "POST", + `${this.baseUrl}/repository/files/${encodedFilepath}` + ); + this._setRequestHeader(request); + + request.send(JSON.stringify(body)); + } + + private _setRequestHeader(request: XMLHttpRequest) { + request.setRequestHeader("Authorization", `Bearer ${this.token}`); + request.setRequestHeader("Content-Type", `application/json`); + } +} diff --git a/src/ui/modules/urlExport.ts b/src/ui/modules/urlExport.ts index b73784f9..d7351235 100644 --- a/src/ui/modules/urlExport.ts +++ b/src/ui/modules/urlExport.ts @@ -3,8 +3,9 @@ import { commands } from '@config/commands' import config from '@config/config' import { PluginMessage } from '@typings/pluginEvent' import { urlExportRequestBody, urlExportSettings } from '@typings/urlExportData' +import { GitlabRepository } from './gitlabRepository' -const responeHandler = (request: XMLHttpRequest): string => { +const responseHandler = (request: XMLHttpRequest): string => { // 401 if (request.status === 401) { return '🚨 401: Check your access token' @@ -40,31 +41,45 @@ const addUrlExportRequestHeaders = (request: XMLHttpRequest, exportSettings: url } } -const addUrlExportRequestEvents = (request: XMLHttpRequest) => { - // on error - request.onerror = (event) => { - // @ts-ignore - parent.postMessage({ +function requestErrorHandler() { + parent.postMessage( + { pluginMessage: { command: commands.closePlugin, payload: { - notification: '🚨 An error occurred while sending the tokens: check your settings & your server.' - } - } as PluginMessage - }, '*') - } - // show message on successful push - request.onload = (progressEvent: ProgressEvent) => { - // @ts-ignore - parent.postMessage({ + notification: + "🚨 An error occurred while sending the tokens: check your settings & your server.", + }, + } as PluginMessage, + }, + "*" + ); +} + +function requestLoadedHandler(request: XMLHttpRequest) { + // @ts-ignore + parent.postMessage( + { pluginMessage: { command: commands.closePlugin, payload: { - notification: responeHandler(progressEvent.target as XMLHttpRequest) - } - } as PluginMessage - }, '*') - } + notification: responseHandler(request), + }, + } as PluginMessage, + }, + "*" + ); +} + +const addUrlExportRequestEvents = (request: XMLHttpRequest) => { + // on error + request.onerror = (_event) => { + requestErrorHandler(); + }; + // show message on successful push + request.onload = (progressEvent: ProgressEvent) => { + requestLoadedHandler(progressEvent.target as XMLHttpRequest); + }; } const generateUrlExportRequestBody = (exportSettings: urlExportSettings, requestBody: urlExportRequestBody) => { @@ -96,6 +111,19 @@ const urlExport = (parent, exportSettings: urlExportSettings, requestBody: urlEx } as PluginMessage }, '*') } + + if (exportSettings.authType === config.key.authType.gitlabCommit) { + const gitlabRepo = new GitlabRepository({ + baseUrl: exportSettings.url, + token: exportSettings.accessToken, + }); + gitlabRepo.upload(requestBody, exportSettings, { + onError: requestErrorHandler, + onLoaded: requestLoadedHandler, + }); + return; + } + // init request const request = new XMLHttpRequest() // send to user defined url diff --git a/src/utilities/base64.ts b/src/utilities/base64.ts new file mode 100644 index 00000000..4eb7d3ec --- /dev/null +++ b/src/utilities/base64.ts @@ -0,0 +1,9 @@ +const utf8ToBase64 = (text: string): string => { + const utf8EncodedBytes = new TextEncoder().encode(text) + const binString = Array.from(utf8EncodedBytes, (byte) => + String.fromCodePoint(byte) + ).join('') + return btoa(binString) +} + +export { utf8ToBase64 }