├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── @types └── image-size │ └── index.d.ts ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── TECHNICAL_OVERVIEW.md ├── package-lock.json ├── package.json ├── resources ├── dark │ ├── icon-add.svg │ ├── icon-calendar.svg │ ├── icon-info.svg │ ├── icon-jira.svg │ ├── icon-link-external.svg │ ├── icon-pin.svg │ ├── icon-pinned.svg │ ├── icon-refresh.svg │ └── icon-search.svg ├── docs │ └── sample.gif ├── icon-sidebar.svg ├── icon-zeplin.png └── light │ ├── icon-add.svg │ ├── icon-calendar.svg │ ├── icon-info.svg │ ├── icon-jira.svg │ ├── icon-link-external.svg │ ├── icon-pin.svg │ ├── icon-pinned.svg │ ├── icon-refresh.svg │ └── icon-search.svg ├── src ├── analytics │ ├── Analytics.ts │ └── util │ │ ├── MixpanelHelper.ts │ │ └── analyticsTypeUtil.ts ├── coco │ ├── barrel │ │ ├── codeLens │ │ │ └── AddBarrelCodeLensCreator.ts │ │ ├── command │ │ │ ├── AddProjectCommand.ts │ │ │ └── AddStyleguideCommand.ts │ │ ├── diagnostic │ │ │ └── BarrelDiagnosticCreator.ts │ │ ├── flow │ │ │ └── barrelFlow.ts │ │ └── hover │ │ │ ├── BarrelHoverCreator.ts │ │ │ └── BarrelsPropertyHoverData.ts │ ├── common │ │ └── flow │ │ │ └── commonFlow.ts │ ├── component │ │ ├── codeLens │ │ │ ├── AddComponentsCodeLensCreator.ts │ │ │ ├── ComponentCodeLensCreator.ts │ │ │ └── ComponentCodeLensProvider.ts │ │ ├── command │ │ │ ├── AddComponentCommand.ts │ │ │ ├── AddComponentsCommand.ts │ │ │ └── ShowComponentInConfigCommand.ts │ │ ├── data │ │ │ └── ComponentPathStore.ts │ │ ├── diagnostic │ │ │ └── ComponentDiagnosticCreator.ts │ │ ├── documentLink │ │ │ └── ComponentLinkProvider.ts │ │ ├── fileChange │ │ │ └── componentRenameUtil.ts │ │ ├── flow │ │ │ └── componentFlow.ts │ │ ├── hover │ │ │ └── ComponentsPropertyHoverData.ts │ │ ├── model │ │ │ ├── Component.ts │ │ │ └── ComponentPath.ts │ │ └── util │ │ │ └── componentUtil.ts │ ├── config │ │ ├── codeLens │ │ │ ├── ConfigCodeLensProvider.ts │ │ │ ├── ConfigStatisticsCodeLensCreator.ts │ │ │ ├── CustomConfigCodeLensProvider.ts │ │ │ ├── SetConfigCodeLensCreator.ts │ │ │ └── SetConfigRootCodeLensCreator.ts │ │ ├── command │ │ │ ├── CreateConfigCommand.ts │ │ │ ├── OpenConfigCommand.ts │ │ │ ├── SetConfigCommand.ts │ │ │ ├── SetConfigRootCommand.ts │ │ │ └── UnsetConfigCommand.ts │ │ ├── diagnostic │ │ │ └── ConfigDiagnosticsProvider.ts │ │ ├── fileChange │ │ │ └── configRenameUtil.ts │ │ ├── flow │ │ │ └── configFlow.ts │ │ ├── hover │ │ │ └── ConfigHoverCreator.ts │ │ ├── model │ │ │ └── Config.ts │ │ └── util │ │ │ ├── ConfigFolderPaths.ts │ │ │ ├── ConfigPaths.ts │ │ │ ├── CustomConfigs.ts │ │ │ └── configUtil.ts │ ├── link │ │ ├── command │ │ │ └── AddLinkCommand.ts │ │ ├── flow │ │ │ └── linkFlow.ts │ │ ├── hover │ │ │ └── LinksPropertyHoverData.ts │ │ └── model │ │ │ └── Link.ts │ ├── plugin │ │ ├── command │ │ │ └── AddPluginCommand.ts │ │ ├── flow │ │ │ └── pluginFlow.ts │ │ ├── hover │ │ │ └── PluginsPropertyHoverData.ts │ │ └── model │ │ │ └── Plugin.ts │ ├── repository │ │ ├── command │ │ │ └── AddRepositoryCommand.ts │ │ ├── flow │ │ │ └── repositoryFlow.ts │ │ ├── hover │ │ │ └── RepositoryPropertyHoverData.ts │ │ ├── model │ │ │ ├── Repository.ts │ │ │ └── RepositoryType.ts │ │ └── util │ │ │ └── repositoryUtil.ts │ └── zeplinComponent │ │ ├── codeLens │ │ ├── AddZeplinComponentsCodeLensCreator.ts │ │ └── MigrateZeplinComponentsCodeLensCreator.ts │ │ ├── command │ │ ├── AddZeplinComponentsCommand.ts │ │ └── MigrateZeplinComponentsCommand.ts │ │ ├── data │ │ ├── ComponentStore.ts │ │ ├── ConfigBarrelsStore.ts │ │ ├── ConfigZeplinComponentsStore.ts │ │ └── ZeplinComponentStore.ts │ │ ├── diagnostic │ │ └── ZeplinComponentDiagnosticCreator.ts │ │ ├── fileOperation │ │ └── askZeplinComponentMigrationUtil.ts │ │ ├── flow │ │ ├── migrateZeplinComponentsFlow.ts │ │ └── zeplinComponentFlow.ts │ │ ├── hover │ │ └── ZeplinComponentHoverCreator.ts │ │ ├── model │ │ └── ZeplinComponentMigrationResult.ts │ │ └── util │ │ ├── zeplinComponentMigrationUtil.ts │ │ └── zeplinComponentUtil.ts ├── common │ ├── domain │ │ ├── api │ │ │ └── api.ts │ │ ├── barrel │ │ │ ├── Barrel.ts │ │ │ ├── BarrelType.ts │ │ │ ├── data │ │ │ │ ├── BarrelDetailsStore.ts │ │ │ │ ├── BarrelStore.ts │ │ │ │ ├── BarrelsStoreProvider.ts │ │ │ │ ├── OrganizationProjectsStore.ts │ │ │ │ ├── OrganizationStyleguidesStore.ts │ │ │ │ ├── PersonalProjectsStore.ts │ │ │ │ ├── PersonalStyleguidesStore.ts │ │ │ │ ├── ProjectDetailsStore.ts │ │ │ │ ├── StyleguideDetailsStore.ts │ │ │ │ └── WorkspacesStore.ts │ │ │ ├── flow │ │ │ │ └── barrelFlow.ts │ │ │ ├── model │ │ │ │ ├── BarrelDetailsResponse.ts │ │ │ │ ├── OrganizationsResponse.ts │ │ │ │ ├── ProjectDetailsResponse.ts │ │ │ │ ├── ProjectsResponse.ts │ │ │ │ ├── ResponseBarrel.ts │ │ │ │ ├── StyleguideDetailsResponse.ts │ │ │ │ ├── StyleguidesResponse.ts │ │ │ │ └── Workspace.ts │ │ │ └── util │ │ │ │ ├── barrelUi.ts │ │ │ │ └── barrelUtil.ts │ │ ├── componentLike │ │ │ └── model │ │ │ │ ├── ComponentLike.ts │ │ │ │ ├── User.ts │ │ │ │ └── Version.ts │ │ ├── error │ │ │ ├── BaseError.ts │ │ │ └── errorUi.ts │ │ ├── extension │ │ │ ├── configuration.ts │ │ │ ├── extension.ts │ │ │ └── zeplinExtensionUtil.ts │ │ ├── hover │ │ │ ├── ConfigProperty.ts │ │ │ ├── ConfigPropertyHoverCreator.ts │ │ │ ├── ConfigPropertyHoverData.ts │ │ │ └── zeplinHoverUtil.ts │ │ ├── image │ │ │ └── zeplinImageUtil.ts │ │ ├── jira │ │ │ ├── model │ │ │ │ ├── Jira.ts │ │ │ │ ├── JiraAttachable.ts │ │ │ │ └── JiraType.ts │ │ │ └── util │ │ │ │ └── jiraUtil.ts │ │ ├── openInZeplin │ │ │ ├── command │ │ │ │ └── OpenInZeplinCommand.ts │ │ │ ├── flow │ │ │ │ └── openInZeplinFlow.ts │ │ │ ├── model │ │ │ │ ├── ApplicationType.ts │ │ │ │ ├── ZeplinLinkType.ts │ │ │ │ ├── ZeplinUriProvider.ts │ │ │ │ └── ZeplinUris.ts │ │ │ └── util │ │ │ │ ├── openInZeplinUtil.ts │ │ │ │ └── zeplinUris.ts │ │ ├── screen │ │ │ └── model │ │ │ │ └── ResponseScreenSection.ts │ │ ├── store │ │ │ ├── BasicStore.ts │ │ │ ├── CacheHolder.ts │ │ │ ├── Result.ts │ │ │ ├── StaticStore.ts │ │ │ └── Store.ts │ │ ├── tree │ │ │ └── TreeItemContext.ts │ │ ├── uri │ │ │ └── UriHandler.ts │ │ └── zeplinComponent │ │ │ ├── data │ │ │ ├── BarrelDetailsStoreProvider.ts │ │ │ └── ZeplinComponentsStore.ts │ │ │ ├── model │ │ │ ├── BarrelDetails.ts │ │ │ ├── BarrelError.ts │ │ │ ├── ResponseZeplinComponent.ts │ │ │ ├── ZeplinComponent.ts │ │ │ ├── ZeplinComponentDescriptors.ts │ │ │ └── ZeplinComponentSection.ts │ │ │ └── util │ │ │ ├── zeplinComponentUi.ts │ │ │ └── zeplinComponentUtil.ts │ ├── general │ │ ├── arrayUtil.ts │ │ ├── booleanUtil.ts │ │ ├── dateTimeUtil.ts │ │ ├── iconPathUtil.ts │ │ ├── imageUtil.ts │ │ ├── pathUtil.ts │ │ ├── promiseUtil.ts │ │ └── stringUtil.ts │ └── vscode │ │ ├── codeLens │ │ ├── CodeLensCreator.ts │ │ ├── CodeLensProvider.ts │ │ └── codeLensUtil.ts │ │ ├── command │ │ └── Command.ts │ │ ├── diagnostic │ │ ├── DiagnosticCreator.ts │ │ └── diagnosticsUtil.ts │ │ ├── editor │ │ ├── editorUtil.ts │ │ └── textDocumentUtil.ts │ │ ├── extension │ │ ├── ContextProvider.ts │ │ └── extensionUtil.ts │ │ ├── fileChange │ │ └── FileChanges.ts │ │ ├── hover │ │ ├── HoverBuilder.ts │ │ ├── HoverProvider.ts │ │ └── hoverUtil.ts │ │ ├── ide │ │ ├── builtinCommands.ts │ │ ├── vscodeFeatureVersions.ts │ │ └── vscodeUtil.ts │ │ ├── message │ │ ├── MessageBuilder.ts │ │ └── MessageType.ts │ │ ├── quickPick │ │ └── QuickPickerProvider.ts │ │ ├── tree │ │ ├── ExpandedErrorTreeItem.ts │ │ ├── TreeDataProvider.ts │ │ ├── TreeItem.ts │ │ └── TreeItemContextProvider.ts │ │ ├── uri │ │ └── uriUtil.ts │ │ └── workspace │ │ └── workspaceUtil.ts ├── localization │ ├── index.ts │ └── localization.ts ├── log │ ├── Logger.ts │ ├── command │ │ └── SaveLogsCommand.ts │ └── util │ │ └── logUtil.ts ├── preferences │ └── Preferences.ts ├── session │ ├── Session.ts │ ├── codeLens │ │ ├── ClearCacheCodeLensCreator.ts │ │ └── LoginCodeLensCreator.ts │ ├── command │ │ ├── ClearCacheCommand.ts │ │ ├── LoginCommand.ts │ │ ├── LogoutCommand.ts │ │ └── ManualLoginCommand.ts │ ├── flow │ │ └── sessionFlow.ts │ ├── tokenStorage │ │ ├── KeychainTokenStorage.ts │ │ ├── TokenStorage.ts │ │ ├── VscodeTokenStorage.ts │ │ └── index.ts │ └── util │ │ ├── Refresher.ts │ │ └── SessionInitializer.ts └── sidebar │ ├── activity │ ├── data │ │ └── ActivityStore.ts │ ├── model │ │ ├── Activity.ts │ │ ├── ComponentActivity.ts │ │ └── ScreenActivity.ts │ ├── tree │ │ ├── ActivityErrorsTreeItem.ts │ │ ├── ActivitySlotTreeItem.ts │ │ ├── ActivityTreeDataProvider.ts │ │ └── ActivityTreeItem.ts │ └── util │ │ └── activityUi.ts │ ├── barrel │ ├── command │ │ ├── AddBarrelToSidebarCommand.ts │ │ ├── AddProjectToSidebarCommand.ts │ │ ├── AddStyleguideToSidebarCommand.ts │ │ └── RemoveBarrelFromSidebarCommand.ts │ ├── flow │ │ └── barrelFlow.ts │ ├── tree │ │ ├── AddBarrelTreeItem.ts │ │ ├── BarrelTreeDataProvider.ts │ │ ├── BarrelTreeItem.ts │ │ └── NoBarrelTreeItem.ts │ └── util │ │ └── barrelUtil.ts │ ├── jira │ ├── command │ │ └── OpenJiraLinkCommand.ts │ └── flow │ │ └── jiraFlow.ts │ ├── jumpTo │ ├── command │ │ └── JumpToSidebarItemCommand.ts │ ├── data │ │ └── JumpableItemsStore.ts │ ├── flow │ │ └── jumpToFlow.ts │ └── tree │ │ └── JumpToTreeItem.ts │ ├── openInZeplin │ └── command │ │ └── OpenInZeplinOnDoubleClickCommand.ts │ ├── pin │ ├── command │ │ ├── PinComponentToSidebarCommand.ts │ │ ├── PinScreenToSidebarCommand.ts │ │ ├── PinToSidebarCommand.ts │ │ ├── UnpinAllFromSidebarCommand.ts │ │ └── UnpinFromSidebarCommand.ts │ ├── data │ │ ├── PinnableComponentsStore.ts │ │ └── PinnableScreensStore.ts │ ├── flow │ │ └── pinFlow.ts │ ├── model │ │ ├── ComponentPinData.ts │ │ ├── PinData.ts │ │ ├── PinType.ts │ │ └── ScreenPinData.ts │ ├── tree │ │ ├── NoPinnedItemTreeItem.ts │ │ └── PinTreeDataProvider.ts │ └── util │ │ └── pinUtil.ts │ ├── refresh │ ├── command │ │ └── RefreshSidebarCommand.ts │ └── util │ │ └── refreshUtil.ts │ ├── screen │ ├── data │ │ ├── ProjectScreensStore.ts │ │ ├── ScreensStore.ts │ │ └── ScreensStoreProvider.ts │ ├── model │ │ ├── ProjectScreens.ts │ │ ├── ResponseScreen.ts │ │ ├── Screen.ts │ │ ├── ScreenSection.ts │ │ ├── ScreensError.ts │ │ └── ScreensResponse.ts │ ├── tree │ │ ├── ScreenSectionTreeItem.ts │ │ ├── ScreenTreeItem.ts │ │ └── ScreensTreeItem.ts │ └── util │ │ └── screenUi.ts │ └── zeplinComponent │ ├── data │ └── CumulativeBarrelDetailsStore.ts │ └── tree │ ├── BarrelZeplinComponentsTreeItem.ts │ ├── ZeplinComponentSectionTreeItem.ts │ ├── ZeplinComponentTreeItem.ts │ ├── ZeplinComponentsTreeItem.ts │ └── zeplinComponentTreeUtil.ts ├── tsconfig.json └── webpack.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [src/**.{ts,json,js}] 4 | indent_style = space 5 | indent_size = 4 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | end_of_line = lf 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | jest: true 5 | }, 6 | extends: [ 7 | "@zeplin/eslint-config/node", 8 | "plugin:import/errors", 9 | "plugin:import/warnings", 10 | "plugin:import/typescript", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | parser: "@typescript-eslint/parser", 14 | parserOptions: { 15 | "ecmaVersion": 2018, 16 | "sourceType": "module" 17 | }, 18 | plugins: ["@typescript-eslint"], 19 | settings: { 20 | "import/resolver": { 21 | typescript: { directory: "./tsconfig.json" }, 22 | } 23 | }, 24 | rules: { 25 | "capitalized-comments": "error", 26 | "arrow-body-style": ["error", "as-needed"], 27 | "@typescript-eslint/explicit-member-accessibility": "error", 28 | "no-sync": "off", 29 | "no-process-exit": "off", 30 | "no-process-env": "off", 31 | "@typescript-eslint/no-explicit-any": ["error", { "ignoreRestArgs": true }], 32 | "@typescript-eslint/camelcase": ["error", { "properties": "never", "ignoreDestructuring": true }], 33 | "class-methods-use-this": "off", 34 | "no-else-return": "off", 35 | "prefer-destructuring": "off", 36 | "@typescript-eslint/no-non-null-assertion": "off", 37 | "@typescript-eslint/explicit-function-return-type": "off", 38 | "import/no-unresolved": "off", 39 | "no-undefined": "off", 40 | "@typescript-eslint/no-use-before-define": "off", 41 | "semi": "off", // On by next line for ts 42 | "@typescript-eslint/semi": "error", 43 | "no-await-in-loop": "off", 44 | "no-invalid-this": "off", 45 | "no-magic-numbers": "off", // On by next line for ts 46 | "@typescript-eslint/no-magic-numbers": ["error", { "ignore": [0, 1], "ignoreEnums": true }], 47 | "valid-jsdoc": "off", // This rule is disabled because "requireReturn: false" results in false positives 48 | "@typescript-eslint/no-empty-interface": "off", 49 | "handle-callback-err": "off" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | out 3 | node_modules 4 | .vscode-test/ 5 | *.vsix -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-vscode.vscode-typescript-tslint-plugin" 4 | ] 5 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Run Extension", 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "runtimeExecutable": "${execPath}", 9 | "args": [ 10 | "--extensionDevelopmentPath=${workspaceFolder}" 11 | ], 12 | "outFiles": [ 13 | "${workspaceFolder}/dist/*.js" 14 | ], 15 | "preLaunchTask": "npm: compile" 16 | }, 17 | { 18 | "name": "Extension Tests", 19 | "type": "extensionHost", 20 | "request": "launch", 21 | "runtimeExecutable": "${execPath}", 22 | "args": [ 23 | "--extensionDevelopmentPath=${workspaceFolder}", 24 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 25 | ], 26 | "outFiles": [ 27 | "${workspaceFolder}/out/test/**/*.js" 28 | ], 29 | "preLaunchTask": "npm: watch" 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "out": false 4 | }, 5 | "search.exclude": { 6 | "out": true 7 | }, 8 | "typescript.tsc.autoDetect": "off" 9 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "watch", 7 | "problemMatcher": "$tsc-watch", 8 | "isBackground": true, 9 | "presentation": { 10 | "reveal": "never" 11 | }, 12 | "group": { 13 | "kind": "build", 14 | "isDefault": true 15 | } 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out 4 | src/** 5 | resources/docs/** 6 | CONTRIBUTING.md 7 | TECHNICAL_OVERVIEW.md 8 | .gitignore 9 | vsc-extension-quickstart.md 10 | node_modules 11 | tsconfig.json 12 | tslint.json 13 | webpack.config.js 14 | **/*.map 15 | **/*.ts -------------------------------------------------------------------------------- /@types/image-size/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "image-size" { 2 | function imageSize(buffer: Buffer): Promise<{ width: number; height: number }>; 3 | 4 | export = imageSize; 5 | } 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.1.0 2 | 3 | - Adds double clicking an item on sidebar tree view opens item on Zeplin: Resolves [#76](https://github.com/zeplin/vscode-extension/issues/76) 4 | - Adds hover support for wildcard on Zeplin components for [Connected Components](https://zpl.io/connected-components) configuration file: Discussed in [#73](https://github.com/zeplin/vscode-extension/issues/73) 5 | - Adds online validation for [Connected Components](https://zpl.io/connected-components) configuration file. 6 | - Fixes blocking clear terminal shortcut: Resolves [#90](https://github.com/zeplin/vscode-extension/issues/90) 7 | 8 | ## 2.0.0 9 | 10 | - Adds Zeplin tree view to VS Code Sidebar 11 | 12 | ## 1.0.0 13 | 14 | - Initial release with [Connected Components](https://zpl.io/connected-components) configuration creation -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Zeplin, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /resources/dark/icon-add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/dark/icon-calendar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/dark/icon-info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/dark/icon-jira.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /resources/dark/icon-link-external.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/dark/icon-pin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/dark/icon-pinned.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/dark/icon-refresh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/dark/icon-search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/docs/sample.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeplin/vscode-extension/7444b74fc789b96c0c25f1bfb8a6d8ca8c635ad9/resources/docs/sample.gif -------------------------------------------------------------------------------- /resources/icon-sidebar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/icon-zeplin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeplin/vscode-extension/7444b74fc789b96c0c25f1bfb8a6d8ca8c635ad9/resources/icon-zeplin.png -------------------------------------------------------------------------------- /resources/light/icon-add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/light/icon-calendar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/light/icon-info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/light/icon-jira.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /resources/light/icon-link-external.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/light/icon-pin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/light/icon-pinned.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/light/icon-refresh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/light/icon-search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/analytics/Analytics.ts: -------------------------------------------------------------------------------- 1 | import MixpanelHelper from "./util/MixpanelHelper"; 2 | import BarrelType from "../common/domain/barrel/BarrelType"; 3 | import PinType from "../sidebar/pin/model/PinType"; 4 | import ZeplinLinkType from "../common/domain/openInZeplin/model/ZeplinLinkType"; 5 | import JiraType from "../common/domain/jira/model/JiraType"; 6 | import { 7 | getAddableTypeName, getPinnableTypeName, getZeplinLinkTypeName, getJiraTypeName 8 | } from "./util/analyticsTypeUtil"; 9 | 10 | class Analytics { 11 | public authenticated() { 12 | MixpanelHelper.track("Authenticated in VS Code extension"); 13 | } 14 | 15 | public resourceAdded(type: BarrelType) { 16 | MixpanelHelper.track( 17 | "Added Zeplin resource in VS Code extension", 18 | { Type: getAddableTypeName(type) } 19 | ); 20 | } 21 | 22 | public resourcePinned(type: PinType) { 23 | MixpanelHelper.track( 24 | "Pinned Zeplin resource in VS Code extension", 25 | { Type: getPinnableTypeName(type) } 26 | ); 27 | } 28 | 29 | public zeplinLinkOpened(type: ZeplinLinkType) { 30 | MixpanelHelper.track( 31 | "Clicked Zeplin resource in VS Code extension", 32 | { Type: getZeplinLinkTypeName(type) } 33 | ); 34 | } 35 | 36 | public jiraLinkOpened(type: JiraType) { 37 | MixpanelHelper.track( 38 | "Clicked Jira issue from VS Code extension", 39 | { Type: getJiraTypeName(type) } 40 | ); 41 | } 42 | } 43 | 44 | export default new Analytics(); 45 | -------------------------------------------------------------------------------- /src/analytics/util/MixpanelHelper.ts: -------------------------------------------------------------------------------- 1 | import Mixpanel from "mixpanel"; 2 | import { decode } from "jsonwebtoken"; 3 | import { getExtensionVersion } from "../../common/vscode/extension/extensionUtil"; 4 | import Session from "../../session/Session"; 5 | import Logger from "../../log/Logger"; 6 | import configuration from "../../common/domain/extension/configuration"; 7 | import Preferences from "../../preferences/Preferences"; 8 | 9 | class MixpanelHelper { 10 | private readonly mixpanel = Mixpanel.init(configuration.mixpanelToken); 11 | 12 | public async track(event: string, additionalProperties?: Mixpanel.PropertyDict, callback?: Mixpanel.Callback) { 13 | if (!Preferences.EnableTelemetry.get()) { 14 | return; 15 | } 16 | 17 | const properties: Mixpanel.PropertyDict = { 18 | "Client": "VS Code extension", 19 | "Client Version": getExtensionVersion(), 20 | ...additionalProperties 21 | }; 22 | 23 | if (Session.isLoggedIn()) { 24 | const token = (await Session.getToken())!; 25 | const decodedToken = decode(token) as { sub: string } | null; 26 | if (decodedToken) { 27 | const userId = decodedToken.sub; 28 | properties.distinct_id = userId; 29 | properties.$user_id = userId; 30 | } 31 | } 32 | 33 | this.mixpanel.track( 34 | event, 35 | properties, 36 | error => { 37 | callback?.(error); 38 | if (error) { 39 | Logger.error(`Mixpanel event '${event}' could not be tracked`, error); 40 | } 41 | } 42 | ); 43 | } 44 | } 45 | 46 | export default new MixpanelHelper(); 47 | -------------------------------------------------------------------------------- /src/analytics/util/analyticsTypeUtil.ts: -------------------------------------------------------------------------------- 1 | import BarrelType from "../../common/domain/barrel/BarrelType"; 2 | import PinType from "../../sidebar/pin/model/PinType"; 3 | import ZeplinLinkType from "../../common/domain/openInZeplin/model/ZeplinLinkType"; 4 | import JiraType from "../../common/domain/jira/model/JiraType"; 5 | 6 | function getAddableTypeName(type: BarrelType) { 7 | switch (type) { 8 | case BarrelType.Project: 9 | return "Project"; 10 | case BarrelType.Styleguide: 11 | return "Styleguide"; 12 | default: 13 | throw new Error(`Unhandled barrel type: ${type}`); 14 | } 15 | } 16 | 17 | function getPinnableTypeName(type: PinType) { 18 | switch (type) { 19 | case PinType.Screen: 20 | return "Screen"; 21 | case PinType.Component: 22 | return "Component"; 23 | default: 24 | throw new Error(`Unhandled pin type: ${type}`); 25 | } 26 | } 27 | 28 | function getZeplinLinkTypeName(type: ZeplinLinkType) { 29 | switch (type) { 30 | case ZeplinLinkType.Project: 31 | return "Project"; 32 | case ZeplinLinkType.Styleguide: 33 | return "Styleguide"; 34 | case ZeplinLinkType.ScreenSection: 35 | return "Screen Section"; 36 | case ZeplinLinkType.ComponentSection: 37 | return "Component Section"; 38 | case ZeplinLinkType.Screen: 39 | return "Screen"; 40 | case ZeplinLinkType.Component: 41 | return "Component"; 42 | default: 43 | throw new Error(`Unhandled Zeplin link type: ${type}`); 44 | } 45 | } 46 | 47 | function getJiraTypeName(type: JiraType) { 48 | switch (type) { 49 | case JiraType.Project: 50 | return "Project"; 51 | case JiraType.ScreenSection: 52 | return "Screen Section"; 53 | case JiraType.Screen: 54 | return "Screen"; 55 | default: 56 | throw new Error(`Unhandled Jira type: ${type}`); 57 | } 58 | } 59 | 60 | export { 61 | getAddableTypeName, 62 | getPinnableTypeName, 63 | getZeplinLinkTypeName, 64 | getJiraTypeName 65 | }; 66 | -------------------------------------------------------------------------------- /src/coco/barrel/codeLens/AddBarrelCodeLensCreator.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import CodeLensCreator from "../../../common/vscode/codeLens/CodeLensCreator"; 3 | import Session from "../../../session/Session"; 4 | import AddProjectCommand from "../command/AddProjectCommand"; 5 | import localization from "../../../localization"; 6 | import AddStyleguideCommand from "../command/AddStyleguideCommand"; 7 | import { getPositionsOfProperty } from "../../../common/vscode/editor/textDocumentUtil"; 8 | import { createCodeLenses } from "../../../common/vscode/codeLens/codeLensUtil"; 9 | import BarrelType from "../../../common/domain/barrel/BarrelType"; 10 | 11 | const KEY_PROJECTS = "projects"; 12 | const KEY_STYLEGUIDES = "styleguides"; 13 | 14 | class AddBarrelCodeLensCreator implements CodeLensCreator { 15 | public create(document: vscode.TextDocument): vscode.CodeLens[] { 16 | if (!Session.isLoggedIn()) { 17 | return []; 18 | } else { 19 | const projectCodeLenses = createCodeLenses( 20 | getPositionsOfProperty(KEY_PROJECTS, document), 21 | { 22 | command: AddProjectCommand.name, 23 | title: localization.common.barrel.add(BarrelType.Project) 24 | } 25 | ); 26 | const styleguideCodeLenses = createCodeLenses( 27 | getPositionsOfProperty(KEY_STYLEGUIDES, document), 28 | { 29 | command: AddStyleguideCommand.name, 30 | title: localization.common.barrel.add(BarrelType.Styleguide) 31 | } 32 | ); 33 | 34 | return projectCodeLenses.concat(styleguideCodeLenses); 35 | } 36 | } 37 | } 38 | 39 | export default new AddBarrelCodeLensCreator(); 40 | -------------------------------------------------------------------------------- /src/coco/barrel/command/AddProjectCommand.ts: -------------------------------------------------------------------------------- 1 | import Command from "../../../common/vscode/command/Command"; 2 | import { startAddProjectFlow } from "../flow/barrelFlow"; 3 | 4 | class AddProjectCommand implements Command { 5 | public name = "zeplin.addProject"; 6 | public execute = startAddProjectFlow; 7 | } 8 | 9 | export default new AddProjectCommand(); 10 | -------------------------------------------------------------------------------- /src/coco/barrel/command/AddStyleguideCommand.ts: -------------------------------------------------------------------------------- 1 | import Command from "../../../common/vscode/command/Command"; 2 | import { startAddStyleguideFlow } from "../flow/barrelFlow"; 3 | 4 | class AddStyleguideCommand implements Command { 5 | public name = "zeplin.addStyleguide"; 6 | public execute = startAddStyleguideFlow; 7 | } 8 | 9 | export default new AddStyleguideCommand(); 10 | -------------------------------------------------------------------------------- /src/coco/barrel/diagnostic/BarrelDiagnosticCreator.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import DiagnosticCreator from "../../../common/vscode/diagnostic/DiagnosticCreator"; 3 | import { getConfig } from "../../config/util/configUtil"; 4 | import { isFirstOccurence, flatten } from "../../../common/general/arrayUtil"; 5 | import { isBarrelIdFormatValid } from "../../../common/domain/barrel/util/barrelUtil"; 6 | import localization from "../../../localization"; 7 | import { createDiagnostics } from "../../../common/vscode/diagnostic/diagnosticsUtil"; 8 | import BarrelType from "../../../common/domain/barrel/BarrelType"; 9 | import { Config } from "../../config/model/Config"; 10 | import ConfigBarrelsStore from "../../zeplinComponent/data/ConfigBarrelsStore"; 11 | 12 | class BarrelDiagnosticCreator implements DiagnosticCreator { 13 | public async create(document: vscode.TextDocument): Promise { 14 | const configPath = document.uri.fsPath; 15 | const config = getConfig(configPath); 16 | const invalidFormatBarrelDiagnostics = [ 17 | ...this.createInvalidFormatBarrelDiagnostics(config, BarrelType.Project, document), 18 | ...this.createInvalidFormatBarrelDiagnostics(config, BarrelType.Styleguide, document) 19 | ]; 20 | 21 | const validFormatBarrelErrors = (await new ConfigBarrelsStore(configPath).get()).errors ?? []; 22 | const validFormatBarrelDiagnostics = flatten(validFormatBarrelErrors.map( 23 | ({ id, message }) => createDiagnostics(id, message, document, vscode.DiagnosticSeverity.Warning) 24 | )); 25 | 26 | return [...invalidFormatBarrelDiagnostics, ...validFormatBarrelDiagnostics]; 27 | } 28 | 29 | private createInvalidFormatBarrelDiagnostics(config: Config, type: BarrelType, document: vscode.TextDocument): 30 | vscode.Diagnostic[] { 31 | return createDiagnostics( 32 | config.getBarrels(type).filter(barrelId => !isBarrelIdFormatValid(barrelId)).filter(isFirstOccurence), 33 | localization.coco.barrel.formatNotValid(type), 34 | document 35 | ); 36 | } 37 | } 38 | 39 | export default new BarrelDiagnosticCreator(); 40 | -------------------------------------------------------------------------------- /src/coco/barrel/flow/barrelFlow.ts: -------------------------------------------------------------------------------- 1 | import localization from "../../../localization"; 2 | import BarrelType from "../../../common/domain/barrel/BarrelType"; 3 | import * as configUtil from "../../config/util/configUtil"; 4 | import { showInEditor } from "../../../common/vscode/editor/editorUtil"; 5 | import { selectAndValidateConfig } from "../../common/flow/commonFlow"; 6 | import MessageBuilder from "../../../common/vscode/message/MessageBuilder"; 7 | import MessageType from "../../../common/vscode/message/MessageType"; 8 | import { pickBarrel } from "../../../common/domain/barrel/flow/barrelFlow"; 9 | 10 | function startAddProjectFlow() { 11 | return startAddBarrelFlow(BarrelType.Project); 12 | } 13 | function startAddStyleguideFlow() { 14 | return startAddBarrelFlow(BarrelType.Styleguide); 15 | } 16 | 17 | async function startAddBarrelFlow(type: BarrelType) { 18 | const title = localization.common.barrel.add(type); 19 | 20 | // Validate login and select config, fail if a modifiable config is not selected 21 | const configPath = await selectAndValidateConfig(title); 22 | if (!configPath) { 23 | return; 24 | } 25 | 26 | // Picker barrel 27 | const barrel = await pickBarrel(title, type); 28 | 29 | // Fail if no barrel is selected 30 | if (!barrel) { 31 | return; 32 | } 33 | 34 | // Fail if config contains barrel 35 | if (configUtil.containsBarrel(configPath, barrel)) { 36 | MessageBuilder.with(localization.common.barrel.alreadyAdded(type)).show(); 37 | showInEditor(configPath, { text: barrel.id }); 38 | return; 39 | } 40 | 41 | // Add barrel 42 | configUtil.addBarrel(configPath, barrel); 43 | MessageBuilder.with(localization.coco.barrel.added(type)).setType(MessageType.Info).show(); 44 | showInEditor(configPath, { text: barrel.id, onAdd: true }); 45 | } 46 | 47 | export { 48 | startAddProjectFlow, 49 | startAddStyleguideFlow 50 | }; 51 | -------------------------------------------------------------------------------- /src/coco/barrel/hover/BarrelsPropertyHoverData.ts: -------------------------------------------------------------------------------- 1 | import ConfigPropertyHoverData from "../../../common/domain/hover/ConfigPropertyHoverData"; 2 | import BarrelType from "../../../common/domain/barrel/BarrelType"; 3 | import AddProjectCommand from "../command/AddProjectCommand"; 4 | import AddStyleguideCommand from "../command/AddStyleguideCommand"; 5 | import Command from "../../../common/vscode/command/Command"; 6 | import localization from "../../../localization"; 7 | 8 | class BarrelsPropertyHoverData implements ConfigPropertyHoverData { 9 | public info = localization.coco.barrel.propInfo(this.type); 10 | public extraInfo = localization.coco.barrel.propExtraInfo(this.type); 11 | public optional = false; 12 | public command: { name: string; text: string }; 13 | 14 | public constructor(private type: BarrelType, public key: string, command: Command) { 15 | this.command = { 16 | name: command.name, 17 | text: localization.common.barrel.add(type) 18 | }; 19 | } 20 | } 21 | 22 | export default { 23 | ForProjects: new BarrelsPropertyHoverData(BarrelType.Project, "projects", AddProjectCommand), 24 | ForStyleguides: new BarrelsPropertyHoverData(BarrelType.Styleguide, "styleguides", AddStyleguideCommand) 25 | }; 26 | -------------------------------------------------------------------------------- /src/coco/common/flow/commonFlow.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import * as configUtil from "../../config/util/configUtil"; 3 | import { showInEditor } from "../../../common/vscode/editor/editorUtil"; 4 | import Session from "../../../session/Session"; 5 | import localization from "../../../localization"; 6 | import { showNotLoggedInError, showNoConfigError } from "../../../common/domain/error/errorUi"; 7 | import MessageBuilder from "../../../common/vscode/message/MessageBuilder"; 8 | import { isFileDirty } from "../../../common/vscode/editor/textDocumentUtil"; 9 | import ConfigPaths from "../../config/util/ConfigPaths"; 10 | 11 | async function selectAndValidateConfig(pickerTitle: string, loginRequired = true): 12 | Promise { 13 | // Check if there are any configs, fail if not so 14 | if (!ConfigPaths.any()) { 15 | showNoConfigError(); 16 | return; 17 | } 18 | 19 | // Check if user is logged, fail if not so 20 | if (loginRequired && !Session.isLoggedIn()) { 21 | showNotLoggedInError(); 22 | return; 23 | } 24 | 25 | // Get active config or ask the user to do so, fail if none selected 26 | const configPath = ConfigPaths.getActive() ?? await showConfigPicker(pickerTitle); 27 | if (!configPath) { 28 | return; 29 | } 30 | 31 | // Check if selected config file is saved, fail if not so 32 | if (isFileDirty(configPath)) { 33 | MessageBuilder.with(localization.coco.common.configNotSaved).show(); 34 | showInEditor(configPath); 35 | return; 36 | } 37 | 38 | // Check if selected config is valid, fail if not so 39 | if (!configUtil.isConfigValid(configPath)) { 40 | MessageBuilder.with(localization.coco.common.configInvalid).show(); 41 | showInEditor(configPath); 42 | return; 43 | } 44 | 45 | return configPath; 46 | } 47 | 48 | // TODO: Add title 49 | // TODO: Show only names for selection 50 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 51 | function showConfigPicker(pickerTitle: string): Thenable { 52 | return vscode.window.showQuickPick( 53 | ConfigPaths.getAll(), 54 | { placeHolder: localization.common.selectFolder } 55 | ); 56 | } 57 | 58 | export { 59 | selectAndValidateConfig 60 | }; 61 | -------------------------------------------------------------------------------- /src/coco/component/codeLens/AddComponentsCodeLensCreator.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import CodeLensCreator from "../../../common/vscode/codeLens/CodeLensCreator"; 3 | import Session from "../../../session/Session"; 4 | import { getPositionsOfProperty } from "../../../common/vscode/editor/textDocumentUtil"; 5 | import AddComponentsCommand from "../command/AddComponentsCommand"; 6 | import localization from "../../../localization"; 7 | import { createCodeLenses } from "../../../common/vscode/codeLens/codeLensUtil"; 8 | 9 | const KEY_COMPONENTS = "components"; 10 | 11 | class AddComponentsCodeLensCreator implements CodeLensCreator { 12 | public create(document: vscode.TextDocument): vscode.CodeLens[] { 13 | if (!Session.isLoggedIn()) { 14 | return []; 15 | } else { 16 | const codeLenses = createCodeLenses( 17 | getPositionsOfProperty(KEY_COMPONENTS, document), 18 | { 19 | command: AddComponentsCommand.name, 20 | title: localization.coco.component.addMultiple 21 | } 22 | ); 23 | 24 | return codeLenses; 25 | } 26 | } 27 | } 28 | 29 | export default new AddComponentsCodeLensCreator(); 30 | -------------------------------------------------------------------------------- /src/coco/component/codeLens/ComponentCodeLensCreator.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import CodeLensCreator from "../../../common/vscode/codeLens/CodeLensCreator"; 3 | import * as configUtil from "../../config/util/configUtil"; 4 | import ShowComponentInConfigCommand from "../command/ShowComponentInConfigCommand"; 5 | import localization from "../../../localization"; 6 | import ConfigPaths from "../../config/util/ConfigPaths"; 7 | 8 | class ComponentCodeLensCreator implements CodeLensCreator { 9 | public create(document: vscode.TextDocument): vscode.CodeLens[] { 10 | const filePath = document.uri.fsPath; 11 | if (!ConfigPaths.hasForItem(filePath) || 12 | !configUtil.isConfigValid(ConfigPaths.getForItem(filePath)) || 13 | !configUtil.isComponentAddedToConfig(filePath)) { 14 | return []; 15 | } 16 | 17 | return [new vscode.CodeLens( 18 | new vscode.Range(0, 0, 0, 0), 19 | { 20 | command: ShowComponentInConfigCommand.name, 21 | title: localization.coco.component.zeplinComponentCount( 22 | configUtil.getZeplinComponentsCountOfComponent(filePath) 23 | ), 24 | arguments: [filePath] 25 | } 26 | )]; 27 | } 28 | } 29 | 30 | export default new ComponentCodeLensCreator(); 31 | -------------------------------------------------------------------------------- /src/coco/component/codeLens/ComponentCodeLensProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import CodeLensCreator from "../../../common/vscode/codeLens/CodeLensCreator"; 3 | import CodeLensProvider from "../../../common/vscode/codeLens/CodeLensProvider"; 4 | import ComponentCodeLensCreator from "./ComponentCodeLensCreator"; 5 | 6 | class ComponentCodeLensProvider extends CodeLensProvider { 7 | public getDocumentSelector(): vscode.DocumentSelector { 8 | return { pattern: `**` }; 9 | } 10 | 11 | protected getCodeLensCreators(): CodeLensCreator[] { 12 | return [ 13 | ComponentCodeLensCreator 14 | ]; 15 | } 16 | } 17 | 18 | export default new ComponentCodeLensProvider(); 19 | -------------------------------------------------------------------------------- /src/coco/component/command/AddComponentCommand.ts: -------------------------------------------------------------------------------- 1 | import Command from "../../../common/vscode/command/Command"; 2 | import { startAddComponentFlow } from "../flow/componentFlow"; 3 | 4 | class AddComponentCommand implements Command { 5 | public name = "zeplin.addComponent"; 6 | public execute = startAddComponentFlow; 7 | } 8 | 9 | export default new AddComponentCommand(); 10 | -------------------------------------------------------------------------------- /src/coco/component/command/AddComponentsCommand.ts: -------------------------------------------------------------------------------- 1 | import Command from "../../../common/vscode/command/Command"; 2 | import { startAddComponentsFlow } from "../flow/componentFlow"; 3 | 4 | class AddComponentsCommand implements Command { 5 | public name = "zeplin.addComponents"; 6 | public execute = startAddComponentsFlow; 7 | } 8 | 9 | export default new AddComponentsCommand(); 10 | -------------------------------------------------------------------------------- /src/coco/component/command/ShowComponentInConfigCommand.ts: -------------------------------------------------------------------------------- 1 | import Command from "../../../common/vscode/command/Command"; 2 | import { showComponentInConfig } from "../flow/componentFlow"; 3 | 4 | class ShowComponentInConfigCommand implements Command { 5 | public name = "openInConfig"; 6 | 7 | public execute(item: string) { 8 | showComponentInConfig(item); 9 | return Promise.resolve(); 10 | } 11 | } 12 | 13 | export default new ShowComponentInConfigCommand(); 14 | -------------------------------------------------------------------------------- /src/coco/component/diagnostic/ComponentDiagnosticCreator.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import DiagnosticCreator from "../../../common/vscode/diagnostic/DiagnosticCreator"; 3 | import { getConfig } from "../../config/util/configUtil"; 4 | import { createDiagnostics } from "../../../common/vscode/diagnostic/diagnosticsUtil"; 5 | import localization from "../../../localization"; 6 | import { doesComponentExist } from "../util/componentUtil"; 7 | import { isFirstOccurence } from "../../../common/general/arrayUtil"; 8 | import ConfigPaths from "../../config/util/ConfigPaths"; 9 | 10 | class ComponentDiagnosticCreator implements DiagnosticCreator { 11 | public create(document: vscode.TextDocument): vscode.Diagnostic[] { 12 | const configPath = document.uri.fsPath; 13 | const config = getConfig(configPath); 14 | const rootPath = ConfigPaths.getRootOf(configPath); 15 | const componentRelativePaths = config.getComponents().map(component => component.path); 16 | const nonExistentComponentRelativePaths = componentRelativePaths 17 | .filter(isFirstOccurence) 18 | .filter(relativePath => !doesComponentExist(rootPath, relativePath)); 19 | 20 | return createDiagnostics( 21 | nonExistentComponentRelativePaths, 22 | localization.coco.component.componentNotFound, document 23 | ); 24 | } 25 | } 26 | 27 | export default new ComponentDiagnosticCreator(); 28 | -------------------------------------------------------------------------------- /src/coco/component/documentLink/ComponentLinkProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import * as path from "path"; 3 | import { getConfig, isConfigValid } from "../../config/util/configUtil"; 4 | import { isFirstOccurence, flatten } from "../../../common/general/arrayUtil"; 5 | import { toProperty, getRangesOf } from "../../../common/vscode/editor/textDocumentUtil"; 6 | import localization from "../../../localization"; 7 | import { doesComponentExist } from "../util/componentUtil"; 8 | import { wrapWithLogs } from "../../../log/util/logUtil"; 9 | import ConfigPaths from "../../config/util/ConfigPaths"; 10 | 11 | class ComponentLinkProvider implements vscode.DocumentLinkProvider { 12 | public getDocumentSelector(): vscode.DocumentSelector { 13 | return { pattern: "**" }; 14 | } 15 | 16 | public provideDocumentLinks(document: vscode.TextDocument): vscode.DocumentLink[] { 17 | return wrapWithLogs( 18 | () => { 19 | const configPath = document.uri.fsPath; 20 | if (ConfigPaths.include(configPath) && !isConfigValid(configPath)) { 21 | return []; 22 | } 23 | 24 | const rootPath = ConfigPaths.getRootOf(configPath); 25 | const componentRelativePaths = getConfig(configPath) 26 | .getComponents() 27 | .map(component => component.path) 28 | .filter(relativePath => doesComponentExist(rootPath, relativePath)) 29 | .filter(isFirstOccurence); 30 | const links = componentRelativePaths 31 | .map(relativePath => this.getLinksForRelativePath(relativePath, rootPath, document)); 32 | return flatten(links); 33 | }, 34 | ComponentLinkProvider.name, 35 | true 36 | ); 37 | } 38 | 39 | private getLinksForRelativePath(relativePath: string, rootPath: string, document: vscode.TextDocument): 40 | vscode.DocumentLink[] { 41 | const searchText = toProperty(relativePath); 42 | return getRangesOf(searchText, document).map( 43 | range => ({ 44 | range, 45 | target: vscode.Uri.file(path.join(rootPath, relativePath)), 46 | tooltip: localization.coco.component.goToFile 47 | }) 48 | ); 49 | } 50 | } 51 | 52 | export default new ComponentLinkProvider(); 53 | -------------------------------------------------------------------------------- /src/coco/component/model/Component.ts: -------------------------------------------------------------------------------- 1 | import ZeplinComponentDescriptors from "../../../common/domain/zeplinComponent/model/ZeplinComponentDescriptors"; 2 | 3 | export default interface Component extends ZeplinComponentDescriptors { 4 | path: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/coco/component/model/ComponentPath.ts: -------------------------------------------------------------------------------- 1 | export default interface ComponentPath { 2 | relative: string; 3 | fs: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/coco/component/util/componentUtil.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | 4 | function doesComponentExist(rootPath: string, relativePath: string): boolean { 5 | return fs.existsSync(path.join(rootPath, relativePath)); 6 | } 7 | 8 | export { 9 | doesComponentExist 10 | }; 11 | -------------------------------------------------------------------------------- /src/coco/config/codeLens/ConfigCodeLensProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import ConfigPaths from "../util/ConfigPaths"; 3 | import CodeLensCreator from "../../../common/vscode/codeLens/CodeLensCreator"; 4 | import ClearCacheCodeLensCreator from "../../../session/codeLens/ClearCacheCodeLensCreator"; 5 | import LoginCodeLensCreator from "../../../session/codeLens/LoginCodeLensCreator"; 6 | import ConfigStatisticsCodeLensCreator from "./ConfigStatisticsCodeLensCreator"; 7 | import AddBarrelCodeLensCreator from "../../barrel/codeLens/AddBarrelCodeLensCreator"; 8 | import AddComponentsCodeLensCreator from "../../component/codeLens/AddComponentsCodeLensCreator"; 9 | import AddZeplinComponentsCodeLensCreator from "../../zeplinComponent/codeLens/AddZeplinComponentsCodeLensCreator"; 10 | import CodeLensProvider from "../../../common/vscode/codeLens/CodeLensProvider"; 11 | import SetConfigRootCodeLensCreator from "./SetConfigRootCodeLensCreator"; 12 | import MigrateZeplinComponentsCodeLensCreator from "../../zeplinComponent/codeLens/MigrateZeplinComponentsCodeLensCreator"; 13 | 14 | class ConfigCodeLensProvider extends CodeLensProvider { 15 | public createWatcher(): vscode.Disposable { 16 | return vscode.workspace.onDidSaveTextDocument(document => { 17 | if (ConfigPaths.include(document.uri.fsPath)) { 18 | this.refresh(); 19 | } 20 | }); 21 | } 22 | 23 | public getDocumentSelector(): vscode.DocumentSelector { 24 | return { pattern: "**" }; 25 | } 26 | 27 | protected getCodeLensCreators(document: vscode.TextDocument): CodeLensCreator[] { 28 | if (!ConfigPaths.include(document.uri.fsPath)) { 29 | return []; 30 | } 31 | 32 | return [ 33 | LoginCodeLensCreator, 34 | ClearCacheCodeLensCreator, 35 | ConfigStatisticsCodeLensCreator, 36 | SetConfigRootCodeLensCreator, 37 | AddBarrelCodeLensCreator, 38 | AddComponentsCodeLensCreator, 39 | AddZeplinComponentsCodeLensCreator, 40 | MigrateZeplinComponentsCodeLensCreator 41 | ]; 42 | } 43 | } 44 | 45 | export default new ConfigCodeLensProvider(); 46 | -------------------------------------------------------------------------------- /src/coco/config/codeLens/ConfigStatisticsCodeLensCreator.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import CodeLensCreator from "../../../common/vscode/codeLens/CodeLensCreator"; 3 | import { getConfig, isConfigValid } from "../util/configUtil"; 4 | import { isFileDirty } from "../../../common/vscode/editor/textDocumentUtil"; 5 | import localization from "../../../localization"; 6 | 7 | class ConfigStatisticsCodeLensCreator implements CodeLensCreator { 8 | public create(document: vscode.TextDocument): vscode.CodeLens[] { 9 | if (isFileDirty(document.uri.fsPath)) { 10 | return [this.getLens(localization.coco.config.info.dirty)]; 11 | } 12 | 13 | if (!isConfigValid(document.uri.fsPath)) { 14 | return [this.getLens(localization.coco.config.info.notValid)]; 15 | } 16 | 17 | const config = getConfig(document.uri.fsPath); 18 | const projects = config.getProjects(); 19 | const styleguides = config.getStyleguides(); 20 | const components = config.getComponents(); 21 | const zeplinComponents = config.getAllZeplinComponentDescriptors(); 22 | const zeplinComponentsCount = zeplinComponents.zeplinIds.length + zeplinComponents.zeplinNames.length; 23 | return [ 24 | this.getLens(localization.coco.config.info.projectCount(projects.length)), 25 | this.getLens(localization.coco.config.info.styleguideCount(styleguides.length)), 26 | this.getLens(localization.coco.config.info.componentCount(components.length)), 27 | this.getLens(localization.coco.config.info.zeplinComponentCount(zeplinComponentsCount)) 28 | ]; 29 | } 30 | 31 | private getLens(title: string): vscode.CodeLens { 32 | return new vscode.CodeLens( 33 | new vscode.Range(0, 0, 0, 0), 34 | { 35 | command: "", 36 | title 37 | } 38 | ); 39 | } 40 | } 41 | 42 | export default new ConfigStatisticsCodeLensCreator(); 43 | -------------------------------------------------------------------------------- /src/coco/config/codeLens/CustomConfigCodeLensProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import CodeLensCreator from "../../../common/vscode/codeLens/CodeLensCreator"; 3 | import CodeLensProvider from "../../../common/vscode/codeLens/CodeLensProvider"; 4 | import SetConfigCodeLensCreator from "./SetConfigCodeLensCreator"; 5 | import ConfigPaths from "../util/ConfigPaths"; 6 | 7 | class CustomConfigCodeLensProvider extends CodeLensProvider { 8 | public createWatcher(): vscode.Disposable { 9 | return vscode.workspace.onDidSaveTextDocument(document => { 10 | if (!ConfigPaths.include(document.uri.fsPath)) { 11 | this.refresh(); 12 | } 13 | }); 14 | } 15 | 16 | public getDocumentSelector(): vscode.DocumentSelector { 17 | return { pattern: "**" }; 18 | } 19 | 20 | protected getCodeLensCreators(): CodeLensCreator[] { 21 | return [ 22 | SetConfigCodeLensCreator 23 | ]; 24 | } 25 | } 26 | 27 | export default new CustomConfigCodeLensProvider(); 28 | -------------------------------------------------------------------------------- /src/coco/config/codeLens/SetConfigCodeLensCreator.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import CodeLensCreator from "../../../common/vscode/codeLens/CodeLensCreator"; 3 | import localization from "../../../localization"; 4 | import SetConfigCommand from "../command/SetConfigCommand"; 5 | import ConfigPaths from "../util/ConfigPaths"; 6 | import { isConfigValid } from "../util/configUtil"; 7 | 8 | class SetConfigCodeLensCreator implements CodeLensCreator { 9 | public create(document: vscode.TextDocument): vscode.CodeLens[] { 10 | const filePath = document.uri.fsPath; 11 | if (ConfigPaths.include(filePath) || document.isDirty || !isConfigValid(filePath)) { 12 | return []; 13 | } 14 | 15 | return [new vscode.CodeLens( 16 | new vscode.Range(0, 0, 0, 0), 17 | { 18 | command: SetConfigCommand.name, 19 | title: localization.coco.config.custom.setConfig, 20 | arguments: [filePath] 21 | } 22 | )]; 23 | } 24 | } 25 | 26 | export default new SetConfigCodeLensCreator(); 27 | -------------------------------------------------------------------------------- /src/coco/config/codeLens/SetConfigRootCodeLensCreator.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as vscode from "vscode"; 3 | import CodeLensCreator from "../../../common/vscode/codeLens/CodeLensCreator"; 4 | import localization from "../../../localization"; 5 | import SetConfigRootCommand from "../command/SetConfigRootCommand"; 6 | import CustomConfigs from "../util/CustomConfigs"; 7 | import ConfigPaths from "../util/ConfigPaths"; 8 | import { getRelativePathToRootFolder } from "../../../common/vscode/workspace/workspaceUtil"; 9 | 10 | class SetConfigRootCodeLensCreator implements CodeLensCreator { 11 | public create(document: vscode.TextDocument): vscode.CodeLens[] { 12 | const filePath = document.uri.fsPath; 13 | if (!CustomConfigs.include(filePath)) { 14 | return []; 15 | } 16 | 17 | const rootPath = ConfigPaths.getRootOf(filePath); 18 | const relativeRootPath = getRelativePathToRootFolder(rootPath); 19 | return [new vscode.CodeLens( 20 | new vscode.Range(0, 0, 0, 0), 21 | { 22 | command: SetConfigRootCommand.name, 23 | title: localization.coco.config.custom.currentRoot(path.join(".", relativeRootPath)), 24 | arguments: [filePath] 25 | } 26 | )]; 27 | } 28 | } 29 | 30 | export default new SetConfigRootCodeLensCreator(); 31 | -------------------------------------------------------------------------------- /src/coco/config/command/CreateConfigCommand.ts: -------------------------------------------------------------------------------- 1 | import Command from "../../../common/vscode/command/Command"; 2 | import { tryCreateConfig } from "../flow/configFlow"; 3 | 4 | class CreateConfigCommand implements Command { 5 | public name = "zeplin.createConfig"; 6 | public execute = tryCreateConfig; 7 | } 8 | 9 | export default new CreateConfigCommand(); 10 | -------------------------------------------------------------------------------- /src/coco/config/command/OpenConfigCommand.ts: -------------------------------------------------------------------------------- 1 | import Command from "../../../common/vscode/command/Command"; 2 | import { tryOpenConfig } from "../flow/configFlow"; 3 | 4 | class OpenConfigCommand implements Command { 5 | public name = "zeplin.openConfig"; 6 | public execute = tryOpenConfig; 7 | } 8 | 9 | export default new OpenConfigCommand(); 10 | -------------------------------------------------------------------------------- /src/coco/config/command/SetConfigCommand.ts: -------------------------------------------------------------------------------- 1 | import Command from "../../../common/vscode/command/Command"; 2 | import { startSetConfigFlow } from "../flow/configFlow"; 3 | 4 | class SetConfigCommand implements Command { 5 | public name = "zeplin.setConfig"; 6 | public execute = startSetConfigFlow; 7 | } 8 | 9 | export default new SetConfigCommand(); 10 | -------------------------------------------------------------------------------- /src/coco/config/command/SetConfigRootCommand.ts: -------------------------------------------------------------------------------- 1 | import Command from "../../../common/vscode/command/Command"; 2 | import { startSetConfigRootFlow } from "../flow/configFlow"; 3 | 4 | class SetConfigRootCommand implements Command { 5 | public name = "zeplin.setConfigRoot"; 6 | public execute = startSetConfigRootFlow; 7 | } 8 | 9 | export default new SetConfigRootCommand(); 10 | -------------------------------------------------------------------------------- /src/coco/config/command/UnsetConfigCommand.ts: -------------------------------------------------------------------------------- 1 | import Command from "../../../common/vscode/command/Command"; 2 | import { startUnsetConfigFlow } from "../flow/configFlow"; 3 | 4 | class UnsetConfigCommand implements Command { 5 | public name = "zeplin.unsetConfig"; 6 | public execute = startUnsetConfigFlow; 7 | } 8 | 9 | export default new UnsetConfigCommand(); 10 | -------------------------------------------------------------------------------- /src/coco/config/hover/ConfigHoverCreator.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | export default interface ConfigHoverCreator { 4 | isApplicable(configPath: string, word: string): boolean; 5 | create(configPath: string, word: string): Promise; 6 | } 7 | -------------------------------------------------------------------------------- /src/coco/config/util/ConfigFolderPaths.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import { getRootFolderPaths } from "../../../common/vscode/workspace/workspaceUtil"; 3 | import ConfigPaths from "./ConfigPaths"; 4 | import CustomConfigs from "./CustomConfigs"; 5 | import { isFirstOccurence } from "../../../common/general/arrayUtil"; 6 | import { compareLength } from "../../../common/general/stringUtil"; 7 | import { toUnixPath, removeLeadingPathSeparator } from "../../../common/general/pathUtil"; 8 | 9 | class ConfigFolderPaths { 10 | private getAll(): string[] { 11 | return CustomConfigs 12 | .getAll() 13 | .map(({ rootPath }) => rootPath) 14 | .concat(getRootFolderPaths()) 15 | .filter(isFirstOccurence); 16 | } 17 | 18 | public getAllWithConfig(): string[] { 19 | return CustomConfigs 20 | .getAll() 21 | .map(({ rootPath }) => rootPath) 22 | .concat(getRootFolderPaths().filter(rootPath => fs.existsSync(this.getConfigOf(rootPath)))) 23 | .filter(isFirstOccurence); 24 | } 25 | 26 | public getAllWithNoConfig(): string[] { 27 | return getRootFolderPaths().filter(rootPath => !fs.existsSync(this.getConfigOf(rootPath))); 28 | } 29 | 30 | public exactlyOneWithNoConfig(): boolean { 31 | return this.getAllWithNoConfig().length === 1; 32 | } 33 | 34 | public allHaveConfig(): boolean { 35 | return getRootFolderPaths().map(this.getConfigOf).every(fs.existsSync); 36 | } 37 | 38 | public get(itemPath: string): string { 39 | return this.getAll().sort(compareLength).find(folderPath => itemPath.startsWith(folderPath))!; 40 | } 41 | 42 | public getRelativePathOf(itemPath: string): string { 43 | const rootPath = this.get(itemPath); 44 | const relativePath = toUnixPath(removeLeadingPathSeparator(itemPath.replace(rootPath, ""))); 45 | return relativePath; 46 | } 47 | 48 | public getConfigOf(rootPath: string): string { 49 | return CustomConfigs.getAll().find(descriptor => rootPath === descriptor.rootPath)?.path ?? 50 | ConfigPaths.getDefault(rootPath); 51 | } 52 | } 53 | 54 | export default new ConfigFolderPaths(); 55 | -------------------------------------------------------------------------------- /src/coco/link/command/AddLinkCommand.ts: -------------------------------------------------------------------------------- 1 | import Command from "../../../common/vscode/command/Command"; 2 | import { startAddLinkFlow } from "../flow/linkFlow"; 3 | 4 | class AddLinkCommand implements Command { 5 | public name = "zeplin.addLink"; 6 | public execute = startAddLinkFlow; 7 | } 8 | 9 | export default new AddLinkCommand(); 10 | -------------------------------------------------------------------------------- /src/coco/link/flow/linkFlow.ts: -------------------------------------------------------------------------------- 1 | import MessageBuilder from "../../../common/vscode/message/MessageBuilder"; 2 | import MessageType from "../../../common/vscode/message/MessageType"; 3 | import { selectAndValidateConfig } from "../../common/flow/commonFlow"; 4 | import { addLink } from "../../config/util/configUtil"; 5 | import { showInEditor } from "../../../common/vscode/editor/editorUtil"; 6 | import localization from "../../../localization"; 7 | 8 | async function startAddLinkFlow() { 9 | const configPath = await selectAndValidateConfig(localization.coco.link.add, false); 10 | if (!configPath) { 11 | return; 12 | } 13 | 14 | addLink(configPath); 15 | MessageBuilder.with(localization.coco.link.added).setType(MessageType.Info).show(); 16 | showInEditor(configPath, { text: "", onAdd: true }); 17 | } 18 | 19 | export { 20 | startAddLinkFlow 21 | }; 22 | -------------------------------------------------------------------------------- /src/coco/link/hover/LinksPropertyHoverData.ts: -------------------------------------------------------------------------------- 1 | import AddLinkCommand from "../command/AddLinkCommand"; 2 | import localization from "../../../localization"; 3 | import ConfigPropertyHoverData from "../../../common/domain/hover/ConfigPropertyHoverData"; 4 | 5 | class LinksPropertyHoverData implements ConfigPropertyHoverData { 6 | public key = "links"; 7 | public info = localization.coco.link.propInfo; 8 | public optional = true; 9 | public properties: ConfigPropertyHoverData[] = [ 10 | { 11 | key: "name", 12 | info: localization.coco.link.propNameInfo, 13 | optional: true 14 | }, 15 | { 16 | key: "type", 17 | info: localization.coco.link.propTypeInfo, 18 | extraInfo: localization.coco.link.propTypeExtraInfo, 19 | optional: false 20 | }, 21 | { 22 | key: "url", 23 | info: localization.coco.link.propUrlInfo, 24 | optional: false 25 | } 26 | ]; 27 | public command = { 28 | name: AddLinkCommand.name, 29 | text: localization.coco.link.add 30 | }; 31 | } 32 | 33 | export default new LinksPropertyHoverData(); 34 | -------------------------------------------------------------------------------- /src/coco/link/model/Link.ts: -------------------------------------------------------------------------------- 1 | export default interface Link { 2 | name?: string; 3 | type: string; 4 | url: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/coco/plugin/command/AddPluginCommand.ts: -------------------------------------------------------------------------------- 1 | import Command from "../../../common/vscode/command/Command"; 2 | import { startAddPluginFlow } from "../flow/pluginFlow"; 3 | 4 | class AddPluginCommand implements Command { 5 | public name = "zeplin.addPlugin"; 6 | public execute = startAddPluginFlow; 7 | } 8 | 9 | export default new AddPluginCommand(); 10 | -------------------------------------------------------------------------------- /src/coco/plugin/flow/pluginFlow.ts: -------------------------------------------------------------------------------- 1 | import { selectAndValidateConfig } from "../../common/flow/commonFlow"; 2 | import { addPlugin } from "../../config/util/configUtil"; 3 | import { showInEditor } from "../../../common/vscode/editor/editorUtil"; 4 | import MessageBuilder from "../../../common/vscode/message/MessageBuilder"; 5 | import MessageType from "../../../common/vscode/message/MessageType"; 6 | import localization from "../../../localization"; 7 | 8 | async function startAddPluginFlow() { 9 | const configPath = await selectAndValidateConfig(localization.coco.plugin.add, false); 10 | if (!configPath) { 11 | return; 12 | } 13 | 14 | addPlugin(configPath); 15 | MessageBuilder.with(localization.coco.plugin.added).setType(MessageType.Info).show(); 16 | showInEditor(configPath, { text: "", onAdd: true }); 17 | } 18 | 19 | export { 20 | startAddPluginFlow 21 | }; 22 | -------------------------------------------------------------------------------- /src/coco/plugin/hover/PluginsPropertyHoverData.ts: -------------------------------------------------------------------------------- 1 | import AddPluginCommand from "../command/AddPluginCommand"; 2 | import localization from "../../../localization"; 3 | import ConfigPropertyHoverData from "../../../common/domain/hover/ConfigPropertyHoverData"; 4 | 5 | class PluginsPropertyHoverData implements ConfigPropertyHoverData { 6 | public key = "plugins"; 7 | public info = localization.coco.plugin.propInfo; 8 | public extraInfo = localization.coco.plugin.propExtraInfo; 9 | public optional = true; 10 | public properties: ConfigPropertyHoverData[] = [ 11 | { 12 | key: "name", 13 | info: localization.coco.plugin.propNameInfo, 14 | optional: false 15 | }, 16 | { 17 | key: "config", 18 | info: localization.coco.plugin.propConfigInfo, 19 | optional: true 20 | } 21 | ]; 22 | public command = { 23 | name: AddPluginCommand.name, 24 | text: localization.coco.plugin.add 25 | }; 26 | } 27 | 28 | export default new PluginsPropertyHoverData(); 29 | -------------------------------------------------------------------------------- /src/coco/plugin/model/Plugin.ts: -------------------------------------------------------------------------------- 1 | export default interface Plugin { 2 | name: string; 3 | config?: { 4 | [key: string]: unknown; 5 | }; 6 | } 7 | -------------------------------------------------------------------------------- /src/coco/repository/command/AddRepositoryCommand.ts: -------------------------------------------------------------------------------- 1 | import Command from "../../../common/vscode/command/Command"; 2 | import { startAddRepositoryFlow } from "../flow/repositoryFlow"; 3 | 4 | class AddRepositoryCommand implements Command { 5 | public name = "zeplin.addRepository"; 6 | public execute = startAddRepositoryFlow; 7 | } 8 | 9 | export default new AddRepositoryCommand(); 10 | -------------------------------------------------------------------------------- /src/coco/repository/hover/RepositoryPropertyHoverData.ts: -------------------------------------------------------------------------------- 1 | import ConfigPropertyHoverData from "../../../common/domain/hover/ConfigPropertyHoverData"; 2 | import localization from "../../../localization"; 3 | import RepositoryType from "../model/RepositoryType"; 4 | 5 | class RepositoryPropertyHoverData implements ConfigPropertyHoverData { 6 | public info = localization.coco.repository.propInfo(this.type); 7 | public optional = false; 8 | public properties: ConfigPropertyHoverData[] = [ 9 | { 10 | key: "branch", 11 | info: localization.coco.repository.propBranchInfo(this.type), 12 | optional: true 13 | }, 14 | { 15 | key: "repository", 16 | info: localization.coco.repository.propRepositoryInfo(this.type), 17 | optional: false 18 | }, 19 | { 20 | key: "url", 21 | info: localization.coco.repository.propUrlInfo(this.url), 22 | optional: true 23 | } 24 | ]; 25 | 26 | public constructor(private type: RepositoryType, public key: string, private url: string) { } 27 | } 28 | 29 | export default { 30 | ForGithub: new RepositoryPropertyHoverData(RepositoryType.Github, "github", "https://github.com"), 31 | ForGitlab: new RepositoryPropertyHoverData(RepositoryType.Gitlab, "gitlab", "https://gitlab.com"), 32 | ForBitbucket: new RepositoryPropertyHoverData(RepositoryType.Bitbucket, "bitbucket", "https://bitbucket.org") 33 | }; 34 | -------------------------------------------------------------------------------- /src/coco/repository/model/Repository.ts: -------------------------------------------------------------------------------- 1 | export default interface Repository { 2 | branch?: string; 3 | repository: string; 4 | url?: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/coco/repository/model/RepositoryType.ts: -------------------------------------------------------------------------------- 1 | enum RepositoryType { 2 | Github, 3 | Gitlab, 4 | Bitbucket 5 | } 6 | 7 | export default RepositoryType; 8 | -------------------------------------------------------------------------------- /src/coco/zeplinComponent/codeLens/MigrateZeplinComponentsCodeLensCreator.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import CodeLensCreator from "../../../common/vscode/codeLens/CodeLensCreator"; 3 | import Session from "../../../session/Session"; 4 | import localization from "../../../localization"; 5 | import MigrateZeplinComponentsCommand from "../command/MigrateZeplinComponentsCommand"; 6 | import { containsExplicitZeplinNames } from "../util/zeplinComponentMigrationUtil"; 7 | 8 | class MigrateZeplinComponentsCodeLensCreator implements CodeLensCreator { 9 | public create(document: vscode.TextDocument): vscode.CodeLens[] { 10 | if (!Session.isLoggedIn()) { 11 | return []; 12 | } 13 | 14 | const configPath = document.uri.fsPath; 15 | if (!containsExplicitZeplinNames(configPath)) { 16 | return []; 17 | } 18 | 19 | return [new vscode.CodeLens( 20 | new vscode.Range(0, 0, 0, 0), 21 | { 22 | command: MigrateZeplinComponentsCommand.name, 23 | title: localization.coco.zeplinComponent.migrate, 24 | arguments: [configPath] 25 | } 26 | )]; 27 | } 28 | } 29 | 30 | export default new MigrateZeplinComponentsCodeLensCreator(); 31 | -------------------------------------------------------------------------------- /src/coco/zeplinComponent/command/AddZeplinComponentsCommand.ts: -------------------------------------------------------------------------------- 1 | import Command from "../../../common/vscode/command/Command"; 2 | import { startAddZeplinComponentsFlow } from "../flow/zeplinComponentFlow"; 3 | 4 | class AddZeplinComponentsCommand implements Command { 5 | public name = "zeplin.addZeplinComponents"; 6 | public execute = startAddZeplinComponentsFlow; 7 | } 8 | 9 | export default new AddZeplinComponentsCommand(); 10 | -------------------------------------------------------------------------------- /src/coco/zeplinComponent/command/MigrateZeplinComponentsCommand.ts: -------------------------------------------------------------------------------- 1 | import Command from "../../../common/vscode/command/Command"; 2 | import { startMigrateZeplinComponentsFlow } from "../flow/migrateZeplinComponentsFlow"; 3 | 4 | class MigrateZeplinComponentsCommand implements Command { 5 | public name = "zeplin.migrateZeplinComponents"; 6 | public execute = startMigrateZeplinComponentsFlow; 7 | } 8 | 9 | export default new MigrateZeplinComponentsCommand(); 10 | -------------------------------------------------------------------------------- /src/coco/zeplinComponent/data/ComponentStore.ts: -------------------------------------------------------------------------------- 1 | import Store from "../../../common/domain/store/Store"; 2 | import { getConfig } from "../../config/util/configUtil"; 3 | import Component from "../../component/model/Component"; 4 | import Result from "../../../common/domain/store/Result"; 5 | 6 | export class ComponentStore implements Store { 7 | public constructor(private configPath: string) { } 8 | 9 | public get = (): Promise> => Promise.resolve({ 10 | data: getConfig(this.configPath).getComponents() 11 | }); 12 | 13 | public refresh = this.get; 14 | } 15 | -------------------------------------------------------------------------------- /src/coco/zeplinComponent/data/ConfigBarrelsStore.ts: -------------------------------------------------------------------------------- 1 | import Store from "../../../common/domain/store/Store"; 2 | import { getConfig } from "../../config/util/configUtil"; 3 | import BarrelDetailsStoreProvider from "../../../common/domain/zeplinComponent/data/BarrelDetailsStoreProvider"; 4 | import BarrelDetails from "../../../common/domain/zeplinComponent/model/BarrelDetails"; 5 | import Result from "../../../common/domain/store/Result"; 6 | import { flatten } from "../../../common/general/arrayUtil"; 7 | import BarrelError from "../../../common/domain/zeplinComponent/model/BarrelError"; 8 | 9 | export default class ConfigBarrelsStore implements Store { 10 | public constructor(private path: string) { } 11 | 12 | public get = async (): Promise> => { 13 | const config = getConfig(this.path); 14 | const barrels = config.getValidBarrelsWithTypes(); 15 | const results = await Promise.all( 16 | barrels.map(({ id, type }) => BarrelDetailsStoreProvider.get(id, type).get()) 17 | ); 18 | 19 | return { 20 | data: results.filter(result => result.data).map(result => result.data!), 21 | errors: flatten(results.map(result => result.errors)) 22 | }; 23 | }; 24 | 25 | public refresh = (): Promise> => { 26 | BarrelDetailsStoreProvider.clearCache(); 27 | return this.get(); 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /src/coco/zeplinComponent/data/ConfigZeplinComponentsStore.ts: -------------------------------------------------------------------------------- 1 | import Result from "../../../common/domain/store/Result"; 2 | import Store from "../../../common/domain/store/Store"; 3 | import BarrelDetailsStoreProvider from "../../../common/domain/zeplinComponent/data/BarrelDetailsStoreProvider"; 4 | import ZeplinComponentsStore from "../../../common/domain/zeplinComponent/data/ZeplinComponentsStore"; 5 | import BarrelError from "../../../common/domain/zeplinComponent/model/BarrelError"; 6 | import ZeplinComponent from "../../../common/domain/zeplinComponent/model/ZeplinComponent"; 7 | import { flatten } from "../../../common/general/arrayUtil"; 8 | import { getConfig } from "../../config/util/configUtil"; 9 | 10 | export default class ConfigZeplinComponentsStore implements Store { 11 | public constructor(private configPath: string) { } 12 | 13 | public get = async (): Promise> => { 14 | const config = getConfig(this.configPath); 15 | const barrels = config.getValidBarrelsWithTypes(); 16 | const results = await Promise.all(barrels.map(({ id, type }) => new ZeplinComponentsStore(id, type).get())); 17 | 18 | return { 19 | data: flatten(results.map(result => result.data)), 20 | errors: flatten(results.map(result => result.errors)) 21 | }; 22 | }; 23 | 24 | public refresh = (): Promise> => { 25 | BarrelDetailsStoreProvider.clearCache(); 26 | return this.get(); 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/coco/zeplinComponent/fileOperation/askZeplinComponentMigrationUtil.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import ConfigPaths from "../../config/util/ConfigPaths"; 3 | import { askZeplinComponentMigrationIfNeeded } from "../flow/migrateZeplinComponentsFlow"; 4 | 5 | function askZeplinComponentMigrationOnConfigOpen(document: vscode.TextDocument) { 6 | const filePath = document.uri.fsPath; 7 | if (ConfigPaths.include(filePath)) { 8 | askZeplinComponentMigrationIfNeeded(filePath, false); 9 | } 10 | } 11 | 12 | export { 13 | askZeplinComponentMigrationOnConfigOpen 14 | }; 15 | -------------------------------------------------------------------------------- /src/coco/zeplinComponent/model/ZeplinComponentMigrationResult.ts: -------------------------------------------------------------------------------- 1 | enum ZeplinComponentMigrationResult { 2 | Migrated, 3 | NotMigrated, 4 | Canceled 5 | } 6 | 7 | export default ZeplinComponentMigrationResult; 8 | -------------------------------------------------------------------------------- /src/common/domain/barrel/Barrel.ts: -------------------------------------------------------------------------------- 1 | import BarrelType from "./BarrelType"; 2 | import Jira from "../jira/model/Jira"; 3 | import JiraAttachable from "../jira/model/JiraAttachable"; 4 | 5 | export default interface Barrel extends JiraAttachable { 6 | id: string; 7 | name: string; 8 | type: BarrelType; 9 | platform: string; 10 | densityScale?: number; 11 | thumbnail?: string; 12 | itemJiras: { 13 | ofScreens: Jira[]; 14 | ofScreenSections: Jira[]; 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /src/common/domain/barrel/BarrelType.ts: -------------------------------------------------------------------------------- 1 | enum BarrelType { 2 | Project, 3 | Styleguide 4 | } 5 | 6 | export default BarrelType; 7 | -------------------------------------------------------------------------------- /src/common/domain/barrel/data/BarrelDetailsStore.ts: -------------------------------------------------------------------------------- 1 | import BasicStore from "../../store/BasicStore"; 2 | import BarrelDetails from "../../zeplinComponent/model/BarrelDetails"; 3 | import BarrelDetailsResponse from "../model/BarrelDetailsResponse"; 4 | import BarrelType from "../BarrelType"; 5 | import { getComponentsFromSections } from "../../../../coco/zeplinComponent/util/zeplinComponentUtil"; 6 | import BaseError from "../../error/BaseError"; 7 | import { getProjectJiras, getProjectItemJiras } from "../../jira/util/jiraUtil"; 8 | import BarrelError from "../../zeplinComponent/model/BarrelError"; 9 | 10 | export default abstract class BarrelDetailsStore 11 | extends BasicStore { 12 | protected abstract type: BarrelType; 13 | 14 | public constructor(private id: string) { 15 | super(); 16 | } 17 | 18 | protected async fetchData(): Promise { 19 | const result = await this.fetchBarrelDetails(this.id); 20 | return result instanceof BaseError 21 | ? new BarrelError(this.type, this.id, result.message, result.code) 22 | : result; 23 | } 24 | 25 | protected abstract fetchBarrelDetails(id: string): Promise; 26 | 27 | protected abstract getParent(response: T): string | undefined; 28 | 29 | protected extractData(response: T): BarrelDetails { 30 | return { 31 | id: response._id, 32 | name: response.name, 33 | type: this.type, 34 | platform: response.type, 35 | densityScale: response.densityScale, 36 | thumbnail: response.thumbnail, 37 | parentId: this.getParent(response), 38 | description: response.description, 39 | components: getComponentsFromSections(response, this.type), 40 | componentSections: response.componentSections, 41 | screenSections: response.sections, 42 | jiras: getProjectJiras(response), 43 | itemJiras: getProjectItemJiras(response) 44 | }; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/common/domain/barrel/data/BarrelStore.ts: -------------------------------------------------------------------------------- 1 | import ResponseBarrel from "../model/ResponseBarrel"; 2 | import BasicStore from "../../store/BasicStore"; 3 | import Barrel from "../Barrel"; 4 | import BarrelType from "../BarrelType"; 5 | import { getProjectItemJiras, getProjectJiras } from "../../jira/util/jiraUtil"; 6 | 7 | export default abstract class BarrelStore extends BasicStore { 8 | protected abstract type: BarrelType; 9 | 10 | protected abstract extractBarrels(response: R): ResponseBarrel[]; 11 | 12 | protected extractData(response: R): Barrel[] { 13 | const responseBarrels = this.extractBarrels(response); 14 | 15 | return responseBarrels.map(responseBarrel => ({ 16 | id: responseBarrel._id, 17 | name: responseBarrel.name, 18 | type: this.type, 19 | platform: responseBarrel.type, 20 | densityScale: responseBarrel.densityScale, 21 | thumbnail: responseBarrel.thumbnail, 22 | jiras: getProjectJiras(responseBarrel), 23 | itemJiras: getProjectItemJiras(responseBarrel) 24 | })); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/common/domain/barrel/data/BarrelsStoreProvider.ts: -------------------------------------------------------------------------------- 1 | import OrganizationProjectsStore from "./OrganizationProjectsStore"; 2 | import OrganizationStyleguidesStore from "./OrganizationStyleguidesStore"; 3 | import PersonalProjectsStore from "./PersonalProjectsStore"; 4 | import PersonalStyleguidesStore from "./PersonalStyleguidesStore"; 5 | import Barrel from "../Barrel"; 6 | import BarrelType from "../BarrelType"; 7 | import Store from "../../store/Store"; 8 | import CacheHolder from "../../store/CacheHolder"; 9 | 10 | class BarrelsStoreProvider implements CacheHolder { 11 | private personalProjectsStore = new PersonalProjectsStore(); 12 | private personalStyleguidesStore = new PersonalStyleguidesStore(); 13 | private projectsStoreCache: { [organizationId: string]: Store } = {}; 14 | private styleguidesStoreCache: { [organizationId: string]: Store } = {}; 15 | 16 | public get(type: BarrelType, organizationId?: string): Store { 17 | return type === BarrelType.Project 18 | ? this.getProjectsStore(organizationId) 19 | : this.getStyleguidesStore(organizationId); 20 | } 21 | 22 | private getProjectsStore(organizationId?: string): Store { 23 | if (!organizationId) { 24 | return this.personalProjectsStore; 25 | } else { 26 | if (!this.projectsStoreCache[organizationId]) { 27 | this.projectsStoreCache[organizationId] = new OrganizationProjectsStore(organizationId); 28 | } 29 | 30 | return this.projectsStoreCache[organizationId]; 31 | } 32 | } 33 | 34 | private getStyleguidesStore(organizationId?: string): Store { 35 | if (!organizationId) { 36 | return this.personalStyleguidesStore; 37 | } else { 38 | if (!this.styleguidesStoreCache[organizationId]) { 39 | this.styleguidesStoreCache[organizationId] = new OrganizationStyleguidesStore(organizationId); 40 | } 41 | 42 | return this.styleguidesStoreCache[organizationId]; 43 | } 44 | } 45 | 46 | public clearCache() { 47 | this.personalProjectsStore.clearCache(); 48 | this.personalStyleguidesStore.clearCache(); 49 | this.projectsStoreCache = {}; 50 | this.styleguidesStoreCache = {}; 51 | } 52 | } 53 | 54 | export default new BarrelsStoreProvider(); 55 | -------------------------------------------------------------------------------- /src/common/domain/barrel/data/OrganizationProjectsStore.ts: -------------------------------------------------------------------------------- 1 | import { getOrganizationProjects } from "../../api/api"; 2 | import BarrelType from "../BarrelType"; 3 | import BarrelStore from "./BarrelStore"; 4 | import ResponseBarrel from "../model/ResponseBarrel"; 5 | import ProjectsResponse from "../model/ProjectsResponse"; 6 | import BaseError from "../../error/BaseError"; 7 | 8 | export default class OrganizationProjectsStore extends BarrelStore { 9 | protected type = BarrelType.Project; 10 | 11 | public constructor(private id: string) { 12 | super(); 13 | } 14 | 15 | protected fetchData(): Promise { 16 | return getOrganizationProjects(this.id); 17 | } 18 | 19 | protected extractBarrels(response: ProjectsResponse): ResponseBarrel[] { 20 | return response.projects; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/common/domain/barrel/data/OrganizationStyleguidesStore.ts: -------------------------------------------------------------------------------- 1 | import { getOrganizationStyleguides } from "../../api/api"; 2 | import BarrelType from "../BarrelType"; 3 | import BarrelStore from "./BarrelStore"; 4 | import ResponseBarrel from "../model/ResponseBarrel"; 5 | import StyleguidesResponse from "../model/StyleguidesResponse"; 6 | import BaseError from "../../error/BaseError"; 7 | 8 | export default class OrganizationStyleguidesStore extends BarrelStore { 9 | protected type = BarrelType.Styleguide; 10 | 11 | public constructor(private id: string) { 12 | super(); 13 | } 14 | 15 | protected fetchData(): Promise { 16 | return getOrganizationStyleguides(this.id); 17 | } 18 | 19 | protected extractBarrels(response: StyleguidesResponse): ResponseBarrel[] { 20 | return response.styleguides; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/common/domain/barrel/data/PersonalProjectsStore.ts: -------------------------------------------------------------------------------- 1 | import { getPersonalProjects } from "../../api/api"; 2 | import BarrelType from "../BarrelType"; 3 | import BarrelStore from "./BarrelStore"; 4 | import ResponseBarrel from "../model/ResponseBarrel"; 5 | import ProjectsResponse from "../model/ProjectsResponse"; 6 | 7 | export default class PersonalProjectsStore extends BarrelStore { 8 | protected type = BarrelType.Project; 9 | 10 | protected fetchData = getPersonalProjects; 11 | 12 | protected extractBarrels(response: ProjectsResponse): ResponseBarrel[] { 13 | return response.projects; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/common/domain/barrel/data/PersonalStyleguidesStore.ts: -------------------------------------------------------------------------------- 1 | import { getPersonalStyleguides } from "../../api/api"; 2 | import BarrelType from "../BarrelType"; 3 | import BarrelStore from "./BarrelStore"; 4 | import ResponseBarrel from "../model/ResponseBarrel"; 5 | import StyleguidesResponse from "../model/StyleguidesResponse"; 6 | 7 | export default class PersonalStyleguidesStore extends BarrelStore { 8 | protected type = BarrelType.Styleguide; 9 | 10 | protected fetchData = getPersonalStyleguides; 11 | 12 | protected extractBarrels(response: StyleguidesResponse): ResponseBarrel[] { 13 | return response.styleguides; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/common/domain/barrel/data/ProjectDetailsStore.ts: -------------------------------------------------------------------------------- 1 | import { getProjectDetails } from "../../api/api"; 2 | import BarrelDetailsStore from "./BarrelDetailsStore"; 3 | import BarrelType from "../BarrelType"; 4 | import ProjectDetailsResponse from "../model/ProjectDetailsResponse"; 5 | 6 | export default class ProjectDetailsStore extends BarrelDetailsStore { 7 | protected type = BarrelType.Project; 8 | 9 | protected fetchBarrelDetails = getProjectDetails; 10 | 11 | protected getParent(response: ProjectDetailsResponse): string | undefined { 12 | return response.styleguide; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/common/domain/barrel/data/StyleguideDetailsStore.ts: -------------------------------------------------------------------------------- 1 | import { getStyleguideDetails } from "../../api/api"; 2 | import BarrelDetailsStore from "./BarrelDetailsStore"; 3 | import BarrelType from "../BarrelType"; 4 | import StyleguideDetailsResponse from "../model/StyleguideDetailsResponse"; 5 | 6 | export default class StyleguideDetailsStore extends BarrelDetailsStore { 7 | protected type = BarrelType.Styleguide; 8 | private childId?: string; 9 | private childType?: BarrelType; 10 | 11 | protected fetchBarrelDetails(id: string) { 12 | return getStyleguideDetails(id, this.childId, this.childType); 13 | } 14 | 15 | protected getParent(response: StyleguideDetailsResponse): string | undefined { 16 | return response.parent; 17 | } 18 | 19 | public setChild(id: string, type: BarrelType) { 20 | this.childId = id; 21 | this.childType = type; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/common/domain/barrel/data/WorkspacesStore.ts: -------------------------------------------------------------------------------- 1 | import Workspace from "../model/Workspace"; 2 | import * as api from "../../api/api"; 3 | import BasicStore from "../../store/BasicStore"; 4 | import localization from "../../../../localization"; 5 | import OrganizationsResponse from "../model/OrganizationsResponse"; 6 | 7 | class WorkspacesStore extends BasicStore { 8 | protected fetchData = api.getOrganizations; 9 | 10 | protected extractData(response: OrganizationsResponse): Workspace[] { 11 | return [ 12 | { name: localization.coco.barrel.personalWorkspace }, 13 | ...response.organizations 14 | ]; 15 | } 16 | } 17 | 18 | export default new WorkspacesStore(); 19 | -------------------------------------------------------------------------------- /src/common/domain/barrel/flow/barrelFlow.ts: -------------------------------------------------------------------------------- 1 | import Barrel from "../Barrel"; 2 | import QuickPickProvider from "../../../vscode/quickPick/QuickPickerProvider"; 3 | import WorkspacesStore from "../data/WorkspacesStore"; 4 | import localization from "../../../../localization"; 5 | import BarrelsStoreProvider from "../data/BarrelsStoreProvider"; 6 | import { getBarrelDetailRepresentation } from "../util/barrelUi"; 7 | import BarrelType from "../BarrelType"; 8 | 9 | async function pickBarrel(title: string, type: BarrelType): Promise { 10 | // Show workspace picker 11 | const workspaceQuickPickProvider = new QuickPickProvider( 12 | WorkspacesStore, 13 | workspace => ({ 14 | label: workspace.name 15 | }), 16 | localization.common.barrel.noWorkspaceFound 17 | ); 18 | workspaceQuickPickProvider.get().title = title; 19 | workspaceQuickPickProvider.get().placeholder = localization.common.barrel.selectWorkspace; 20 | const workspace = await workspaceQuickPickProvider.startSingleSelection(); 21 | 22 | // Fail if no workspace is selected 23 | if (!workspace) { 24 | return; 25 | } 26 | 27 | // Show barrel picker 28 | const barrelQuickPickProvider = new QuickPickProvider( 29 | BarrelsStoreProvider.get(type, workspace._id), 30 | barrel => ({ 31 | label: barrel.name, 32 | detail: getBarrelDetailRepresentation(barrel) 33 | }), 34 | localization.common.barrel.noneFound(type) 35 | ); 36 | barrelQuickPickProvider.get().title = title; 37 | barrelQuickPickProvider.get().placeholder = localization.common.barrel.select(type, workspace.name); 38 | return barrelQuickPickProvider.startSingleSelection(); 39 | } 40 | 41 | export { 42 | pickBarrel 43 | }; 44 | -------------------------------------------------------------------------------- /src/common/domain/barrel/model/BarrelDetailsResponse.ts: -------------------------------------------------------------------------------- 1 | import ZeplinComponentSection from "../../zeplinComponent/model/ZeplinComponentSection"; 2 | import ResponseBarrel from "./ResponseBarrel"; 3 | import ResponseScreenSection from "../../screen/model/ResponseScreenSection"; 4 | 5 | export default interface BarrelDetailsResponse extends ResponseBarrel { 6 | description?: string; 7 | componentSections: ZeplinComponentSection[]; 8 | sections?: ResponseScreenSection[]; 9 | } 10 | -------------------------------------------------------------------------------- /src/common/domain/barrel/model/OrganizationsResponse.ts: -------------------------------------------------------------------------------- 1 | import Workspace from "./Workspace"; 2 | 3 | export default interface OrganizationsResponse { 4 | organizations: Workspace[]; 5 | } 6 | -------------------------------------------------------------------------------- /src/common/domain/barrel/model/ProjectDetailsResponse.ts: -------------------------------------------------------------------------------- 1 | import BarrelDetailsResponse from "./BarrelDetailsResponse"; 2 | 3 | export default interface ProjectDetailsResponse extends BarrelDetailsResponse { 4 | styleguide?: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/common/domain/barrel/model/ProjectsResponse.ts: -------------------------------------------------------------------------------- 1 | import ResponseBarrel from "./ResponseBarrel"; 2 | 3 | export default interface ProjectsResponse { 4 | projects: ResponseBarrel[]; 5 | } 6 | -------------------------------------------------------------------------------- /src/common/domain/barrel/model/ResponseBarrel.ts: -------------------------------------------------------------------------------- 1 | import Jira from "../../jira/model/Jira"; 2 | 3 | export default interface ResponseBarrel { 4 | _id: string; 5 | name: string; 6 | type: string; 7 | densityScale?: number; 8 | thumbnail?: string; 9 | integration?: { 10 | jira?: { 11 | attachments?: Jira[]; 12 | }; 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/common/domain/barrel/model/StyleguideDetailsResponse.ts: -------------------------------------------------------------------------------- 1 | import BarrelDetailsResponse from "./BarrelDetailsResponse"; 2 | 3 | export default interface StyleguideDetailsResponse extends BarrelDetailsResponse { 4 | parent?: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/common/domain/barrel/model/StyleguidesResponse.ts: -------------------------------------------------------------------------------- 1 | import ResponseBarrel from "./ResponseBarrel"; 2 | 3 | export default interface StyleguidesResponse { 4 | styleguides: ResponseBarrel[]; 5 | } 6 | -------------------------------------------------------------------------------- /src/common/domain/barrel/model/Workspace.ts: -------------------------------------------------------------------------------- 1 | export default interface Workspace { 2 | _id?: string; 3 | name: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/common/domain/barrel/util/barrelUi.ts: -------------------------------------------------------------------------------- 1 | import Barrel from "../Barrel"; 2 | import localization from "../../../../localization"; 3 | 4 | const DETAIL_ITEM_SEPARATOR = ", "; 5 | const PLATFORM_ANDROID = "android"; 6 | const PLATFORM_REPRESENTATIONS: { [apiValue: string]: string } = Object.freeze({ 7 | android: "Android", 8 | base: "Base", 9 | ios: "iOS", 10 | osx: "macOS", 11 | web: "Web" 12 | }); 13 | const DENSITY_SCALE_REPRESENTATIONS_ANDROID: { [s: number]: string } = Object.freeze({ 14 | 1: "mdpi", 15 | 1.5: "hdpi", 16 | 2: "xhdpi", 17 | 3: "xxhdpi", 18 | 4: "xxxhdpi" 19 | }); 20 | 21 | function getPlatformRepresentation(platform: string): string | undefined { 22 | return PLATFORM_REPRESENTATIONS[platform] ?? platform; 23 | } 24 | 25 | function getDensityScaleRepresentation(platform: string, densityScale?: number): string | undefined { 26 | if (densityScale === null || densityScale === undefined) { 27 | return undefined; 28 | } else if (platform === PLATFORM_ANDROID) { 29 | return DENSITY_SCALE_REPRESENTATIONS_ANDROID[densityScale]; 30 | } else { 31 | return `${densityScale}x`; 32 | } 33 | } 34 | 35 | function getBarrelDetailRepresentation(barrel: Barrel): string { 36 | return mergeRepresentations( 37 | getPlatformRepresentation(barrel.platform), 38 | getDensityScaleRepresentation(barrel.platform, barrel.densityScale) 39 | ); 40 | } 41 | 42 | function getBarrelDetailRepresentationWithType(barrel: Barrel): string { 43 | return mergeRepresentations( 44 | localization.common.barrel.barrel(barrel.type), 45 | getPlatformRepresentation(barrel.platform), 46 | getDensityScaleRepresentation(barrel.platform, barrel.densityScale) 47 | ); 48 | } 49 | 50 | function mergeRepresentations(...representations: (string | undefined)[]) { 51 | return representations 52 | .filter(representation => representation) 53 | .join(DETAIL_ITEM_SEPARATOR); 54 | } 55 | 56 | export { 57 | getBarrelDetailRepresentation, 58 | getBarrelDetailRepresentationWithType 59 | }; 60 | -------------------------------------------------------------------------------- /src/common/domain/barrel/util/barrelUtil.ts: -------------------------------------------------------------------------------- 1 | const BARREL_ID_VALIDATION_REGEX = /^[0-9A-Fa-f]{24}$/; 2 | 3 | function isBarrelIdFormatValid(barrelId: string): boolean { 4 | return BARREL_ID_VALIDATION_REGEX.test(barrelId); 5 | } 6 | 7 | export { 8 | isBarrelIdFormatValid 9 | }; 10 | -------------------------------------------------------------------------------- /src/common/domain/componentLike/model/ComponentLike.ts: -------------------------------------------------------------------------------- 1 | import Version from "./Version"; 2 | 3 | export default interface ComponentLike { 4 | _id: string; 5 | name: string; 6 | description?: string; 7 | latestVersion: Version; 8 | } 9 | -------------------------------------------------------------------------------- /src/common/domain/componentLike/model/User.ts: -------------------------------------------------------------------------------- 1 | export default interface User { 2 | username: string; 3 | avatar?: string; 4 | emotar?: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/common/domain/componentLike/model/Version.ts: -------------------------------------------------------------------------------- 1 | import User from "./User"; 2 | 3 | export default interface Version { 4 | created: string; 5 | creator?: User; 6 | snapshot: { 7 | url: string; 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/common/domain/error/BaseError.ts: -------------------------------------------------------------------------------- 1 | import localization from "../../../localization"; 2 | 3 | /** 4 | * Error data for api and store usage. 5 | */ 6 | export default class BaseError { 7 | public constructor( 8 | public message: string = localization.common.defaultError, 9 | public code?: number 10 | ) { 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/common/domain/extension/configuration.ts: -------------------------------------------------------------------------------- 1 | declare let WPC__API_URL: string; 2 | declare let WPC__APP_URI: string; 3 | declare let WPC__WEB_URL: string; 4 | declare let WPC__IMAGE_SERVER_URL: string; 5 | declare let WPC__OAUTH_CLIENT_ID: string; 6 | declare let WPC__CONSOLE_LOGS_ENABLED: string; 7 | declare let WPC__MIXPANEL_TOKEN: string; 8 | 9 | export default { 10 | /** 11 | * Zeplin API url. 12 | */ 13 | apiUrl: WPC__API_URL, 14 | /** 15 | * Zeplin Windows and Mac app uri. 16 | */ 17 | appUri: WPC__APP_URI, 18 | /** 19 | * Zepin Web app url. 20 | */ 21 | webUrl: WPC__WEB_URL, 22 | /** 23 | * Zeplin image server url. 24 | */ 25 | imageServerUrl: WPC__IMAGE_SERVER_URL, 26 | /** 27 | * Client id for OAuth authentication. 28 | */ 29 | oauthClientId: WPC__OAUTH_CLIENT_ID, 30 | /** 31 | * Whether logs are enabled. 32 | */ 33 | consoleLogsEnabled: WPC__CONSOLE_LOGS_ENABLED, 34 | /** 35 | * Mixpanel Token 36 | */ 37 | mixpanelToken: WPC__MIXPANEL_TOKEN 38 | }; 39 | -------------------------------------------------------------------------------- /src/common/domain/extension/zeplinExtensionUtil.ts: -------------------------------------------------------------------------------- 1 | import { extensionInitialized, isExtensionInitialized } from "../../vscode/extension/extensionUtil"; 2 | import { showLoginWarningAfterInstall } from "../../../session/flow/sessionFlow"; 3 | import Session from "../../../session/Session"; 4 | 5 | async function showWarningsAtStartUp() { 6 | if (!isExtensionInitialized() && !Session.isLoggedIn()) { 7 | showLoginWarningAfterInstall(); 8 | } 9 | 10 | await extensionInitialized(); 11 | } 12 | 13 | export { 14 | showWarningsAtStartUp 15 | }; 16 | -------------------------------------------------------------------------------- /src/common/domain/hover/ConfigProperty.ts: -------------------------------------------------------------------------------- 1 | export default interface ConfigProperty { 2 | key?: string; 3 | title?: string; 4 | info?: string; 5 | extraInfo?: string; 6 | optional: boolean; 7 | properties?: ConfigProperty[]; 8 | } 9 | -------------------------------------------------------------------------------- /src/common/domain/hover/ConfigPropertyHoverData.ts: -------------------------------------------------------------------------------- 1 | import ConfigProperty from "./ConfigProperty"; 2 | 3 | export default interface ConfigPropertyHoverData extends ConfigProperty { 4 | command?: { name: string; text: string }; 5 | } 6 | -------------------------------------------------------------------------------- /src/common/domain/hover/zeplinHoverUtil.ts: -------------------------------------------------------------------------------- 1 | import { getMarkdownImage } from "../../vscode/hover/hoverUtil"; 2 | import { isVscodeVersionSufficient, isVscodeVersionEqualTo } from "../../vscode/ide/vscodeUtil"; 3 | import { HOVER_CODICONS_MIN_VERSION, HOVER_CODICONS_BROKEN_VERSION } from "../../vscode/ide/vscodefeatureVersions"; 4 | import { IconTheme, getIconPath } from "../../general/iconPathUtil"; 5 | 6 | function getMarkdownIcon(codicon: string, imagePath: string): string { 7 | return isVscodeVersionSufficient(HOVER_CODICONS_MIN_VERSION) && 8 | !isVscodeVersionEqualTo(HOVER_CODICONS_BROKEN_VERSION) 9 | ? `$(${codicon})` 10 | : getMarkdownImage(getIconPath(imagePath, IconTheme.Dark)); 11 | } 12 | 13 | function getMarkdownLinkExternalIcon(): string { 14 | return getMarkdownIcon("link-external", "icon-link-external.svg"); 15 | } 16 | 17 | function getMarkdownRefreshIcon(): string { 18 | return getMarkdownIcon("refresh", "icon-refresh.svg"); 19 | } 20 | 21 | export { 22 | getMarkdownLinkExternalIcon, 23 | getMarkdownRefreshIcon 24 | }; 25 | -------------------------------------------------------------------------------- /src/common/domain/image/zeplinImageUtil.ts: -------------------------------------------------------------------------------- 1 | import fetch from "node-fetch"; 2 | import configuration from "../extension/configuration"; 3 | import { snakeCaseToPascalCase } from "../../general/stringUtil"; 4 | 5 | let cache: { [key: string]: string | undefined } = {}; 6 | 7 | async function getCroppedImageUrl(url: string, width: number, height: number): Promise { 8 | try { 9 | if (cache[url]) { 10 | return cache[url]; 11 | } 12 | 13 | const response = (await fetch( 14 | `${configuration.imageServerUrl}/${encodeURIComponent(url)}` + 15 | `?w=${width}&cropTop=0&cropLeft=0&cropWidth=${width}&cropHeight=${height}` 16 | )); 17 | 18 | cache[url] = response.ok ? response.url : undefined; 19 | return cache[url]; 20 | } catch { 21 | return undefined; 22 | } 23 | } 24 | 25 | function resetCroppedImageUrlCache() { 26 | cache = {}; 27 | } 28 | 29 | function getEmotarUrl(emotar: string) { 30 | const emotarFileName = emotar === "random" 31 | ? "icRandom.svg" 32 | : `emotar${snakeCaseToPascalCase(emotar)}.png`; 33 | return `${configuration.webUrl}/img/emotars/${emotarFileName}`; 34 | } 35 | 36 | export { 37 | getCroppedImageUrl, 38 | resetCroppedImageUrlCache, 39 | getEmotarUrl 40 | }; 41 | -------------------------------------------------------------------------------- /src/common/domain/jira/model/Jira.ts: -------------------------------------------------------------------------------- 1 | import JiraType from "./JiraType"; 2 | 3 | export default interface Jira { 4 | _id: string; 5 | issueKey: string; 6 | issueUrl: string; 7 | itemId: string; 8 | type: JiraType; 9 | } 10 | -------------------------------------------------------------------------------- /src/common/domain/jira/model/JiraAttachable.ts: -------------------------------------------------------------------------------- 1 | import Jira from "./Jira"; 2 | 3 | export default interface JiraAttachable { 4 | jiras: Jira[]; 5 | } 6 | -------------------------------------------------------------------------------- /src/common/domain/jira/model/JiraType.ts: -------------------------------------------------------------------------------- 1 | 2 | enum JiraType { 3 | Project = "project", 4 | ScreenSection = "section", 5 | Screen = "screen" 6 | } 7 | 8 | export default JiraType; 9 | -------------------------------------------------------------------------------- /src/common/domain/jira/util/jiraUtil.ts: -------------------------------------------------------------------------------- 1 | import JiraType from "../model/JiraType"; 2 | import Jira from "../model/Jira"; 3 | import ResponseBarrel from "../../barrel/model/ResponseBarrel"; 4 | 5 | function getProjectJiras(project: ResponseBarrel): Jira[] { 6 | return project.integration?.jira?.attachments?.filter( 7 | jira => jira.type === JiraType.Project && jira.itemId === project._id 8 | ) ?? []; 9 | } 10 | 11 | function getProjectItemJiras(project: ResponseBarrel): { ofScreenSections: Jira[]; ofScreens: Jira[] } { 12 | const jiras = project.integration?.jira?.attachments ?? []; 13 | return { 14 | ofScreenSections: jiras.filter(jira => jira.type === JiraType.ScreenSection), 15 | ofScreens: jiras.filter(jira => jira.type === JiraType.Screen) 16 | }; 17 | } 18 | 19 | export { 20 | getProjectJiras, 21 | getProjectItemJiras 22 | }; 23 | -------------------------------------------------------------------------------- /src/common/domain/openInZeplin/command/OpenInZeplinCommand.ts: -------------------------------------------------------------------------------- 1 | import Command from "../../../vscode/command/Command"; 2 | import { openInZeplin } from "../flow/openInZeplinFlow"; 3 | 4 | class OpenInZeplinCommand implements Command { 5 | public name = "zeplin.openInZeplin"; 6 | public execute = openInZeplin; 7 | } 8 | 9 | export default new OpenInZeplinCommand(); 10 | -------------------------------------------------------------------------------- /src/common/domain/openInZeplin/model/ApplicationType.ts: -------------------------------------------------------------------------------- 1 | enum ApplicationType { 2 | Web = "Web", 3 | App = "App" 4 | } 5 | 6 | export default ApplicationType; 7 | -------------------------------------------------------------------------------- /src/common/domain/openInZeplin/model/ZeplinLinkType.ts: -------------------------------------------------------------------------------- 1 | enum ZeplinLinkType { 2 | Project, 3 | Styleguide, 4 | ScreenSection, 5 | Screen, 6 | ComponentSection, 7 | Component 8 | } 9 | 10 | export default ZeplinLinkType; 11 | -------------------------------------------------------------------------------- /src/common/domain/openInZeplin/model/ZeplinUriProvider.ts: -------------------------------------------------------------------------------- 1 | import ApplicationType from "./ApplicationType"; 2 | import ZeplinLinkType from "./ZeplinLinkType"; 3 | 4 | export default interface ZeplinUriProvider { 5 | getZeplinUri(applicationType: ApplicationType): string; 6 | 7 | getZeplinLinkType(): ZeplinLinkType; 8 | } 9 | -------------------------------------------------------------------------------- /src/common/domain/openInZeplin/model/ZeplinUris.ts: -------------------------------------------------------------------------------- 1 | import ZeplinLinkType from "./ZeplinLinkType"; 2 | 3 | export default interface ZeplinUris { 4 | appUri: string; 5 | webUrl: string; 6 | type: ZeplinLinkType; 7 | } 8 | -------------------------------------------------------------------------------- /src/common/domain/openInZeplin/util/openInZeplinUtil.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import ApplicationType from "../model/ApplicationType"; 3 | import ContextProvider from "../../../vscode/extension/ContextProvider"; 4 | import Preferences from "../../../../preferences/Preferences"; 5 | 6 | const KEY_APP_TYPE_SELECTED = "preferredAppTypeSelected"; 7 | 8 | function filterPreferredAppType(setting: unknown): ApplicationType { 9 | return setting === ApplicationType.App ? ApplicationType.App : ApplicationType.Web; 10 | } 11 | 12 | function isPreferredAppTypeSelected(): boolean { 13 | return ContextProvider.get().globalState.get(KEY_APP_TYPE_SELECTED) ?? false; 14 | } 15 | 16 | async function setPreferredAppTypeSelected(type: ApplicationType) { 17 | await Preferences.PreferredApplicationType.update(type, vscode.ConfigurationTarget.Global); 18 | await ContextProvider.get().globalState.update(KEY_APP_TYPE_SELECTED, true); 19 | } 20 | 21 | export { 22 | filterPreferredAppType, 23 | isPreferredAppTypeSelected, 24 | setPreferredAppTypeSelected 25 | }; 26 | -------------------------------------------------------------------------------- /src/common/domain/screen/model/ResponseScreenSection.ts: -------------------------------------------------------------------------------- 1 | export default interface ResponseScreenSection { 2 | _id: string; 3 | name: string; 4 | description?: string; 5 | screens: string[]; 6 | } 7 | -------------------------------------------------------------------------------- /src/common/domain/store/CacheHolder.ts: -------------------------------------------------------------------------------- 1 | export default interface CacheHolder { 2 | clearCache(): void; 3 | } 4 | -------------------------------------------------------------------------------- /src/common/domain/store/Result.ts: -------------------------------------------------------------------------------- 1 | import BaseError from "../error/BaseError"; 2 | 3 | /** 4 | * Result data for api and store usage. 5 | */ 6 | export default interface Result { 7 | data?: T; 8 | errors?: E[]; 9 | } 10 | -------------------------------------------------------------------------------- /src/common/domain/store/StaticStore.ts: -------------------------------------------------------------------------------- 1 | import Store from "./Store"; 2 | import Result from "./Result"; 3 | import BaseError from "../error/BaseError"; 4 | 5 | export default class StaticStore implements Store { 6 | public constructor(private data: T) { } 7 | 8 | public get = (): Promise> => Promise.resolve({ 9 | data: this.data 10 | }); 11 | 12 | public refresh = this.get; 13 | } 14 | -------------------------------------------------------------------------------- /src/common/domain/store/Store.ts: -------------------------------------------------------------------------------- 1 | import Result from "./Result"; 2 | import BaseError from "../error/BaseError"; 3 | 4 | /** 5 | * Store for getting and refreshing data from any source. 6 | */ 7 | export default interface Store { 8 | get: () => Promise>; 9 | refresh: () => Promise>; 10 | } 11 | -------------------------------------------------------------------------------- /src/common/domain/tree/TreeItemContext.ts: -------------------------------------------------------------------------------- 1 | enum TreeItemContext { 2 | Barrel = "barrel", 3 | Screen = "screen", 4 | ScreenSection = "screenSection", 5 | ZeplinComponentBarrel = "zeplinComponentBarrel", 6 | ZeplinComponent = "zeplinComponent", 7 | ZeplinComponentSection = "zeplinComponentSection", 8 | Activity = "activity", 9 | ZeplinLink = "zeplinLink", 10 | Jira = "jira", 11 | Pinnable = "pinnable", 12 | Pinned = "pinned" 13 | } 14 | 15 | export default TreeItemContext; 16 | -------------------------------------------------------------------------------- /src/common/domain/uri/UriHandler.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { completeLogin } from "../../../session/flow/sessionFlow"; 3 | import { URLSearchParams } from "url"; 4 | import localization from "../../../localization/localization"; 5 | import MessageBuilder from "../../vscode/message/MessageBuilder"; 6 | import Logger from "../../../log/Logger"; 7 | 8 | const AUTHORIZED = "/authorized"; 9 | const QUERY_PARAMETER_TOKEN = "access_token"; 10 | 11 | class UriHandler implements vscode.UriHandler { 12 | public handleUri(uri: vscode.Uri) { 13 | Logger.log(`Uri received: ${uri.path}`); 14 | 15 | const parameters = new URLSearchParams(uri.query); 16 | switch (uri.path) { 17 | case AUTHORIZED: 18 | completeLogin(parameters.get(QUERY_PARAMETER_TOKEN) as string); 19 | break; 20 | default: 21 | MessageBuilder.with(localization.common.wrongUri).show(); 22 | break; 23 | } 24 | } 25 | } 26 | 27 | export default new UriHandler(); 28 | -------------------------------------------------------------------------------- /src/common/domain/zeplinComponent/data/BarrelDetailsStoreProvider.ts: -------------------------------------------------------------------------------- 1 | import BarrelType from "../../barrel/BarrelType"; 2 | import ProjectDetailsStore from "../../barrel/data/ProjectDetailsStore"; 3 | import StyleguideDetailsStore from "../../barrel/data/StyleguideDetailsStore"; 4 | import CacheHolder from "../../store/CacheHolder"; 5 | import BarrelDetailsStore from "../../barrel/data/BarrelDetailsStore"; 6 | import BarrelDetailsResponse from "../../barrel/model/BarrelDetailsResponse"; 7 | import { updateSidebarItems } from "../../../../sidebar/refresh/util/refreshUtil"; 8 | 9 | class BarrelDetailsStoreProvider implements CacheHolder { 10 | private cache: { [id: string]: BarrelDetailsStore } = {}; 11 | 12 | public get(id: string, type: BarrelType, childId?: string, childType?: BarrelType): 13 | BarrelDetailsStore { 14 | if (!this.cache[id]) { 15 | this.cache[id] = type === BarrelType.Project ? new ProjectDetailsStore(id) : new StyleguideDetailsStore(id); 16 | this.cache[id].onDataReceived(updateSidebarItems); 17 | } 18 | 19 | if (type === BarrelType.Styleguide && childId) { 20 | (this.cache[id] as StyleguideDetailsStore).setChild(childId, childType!); 21 | } 22 | 23 | return this.cache[id]; 24 | } 25 | 26 | public clearCache() { 27 | Object.keys(this.cache).forEach(key => this.cache[key].dispose()); 28 | this.cache = {}; 29 | } 30 | 31 | public clearCacheFor(id: string) { 32 | let currentId: string | undefined = id; 33 | while (currentId && this.cache[currentId]) { 34 | const nextId: string | undefined = this.cache[currentId].getCache()?.data?.parentId; 35 | this.cache[currentId].dispose(); 36 | delete this.cache[currentId]; 37 | currentId = nextId; 38 | } 39 | } 40 | } 41 | 42 | export default new BarrelDetailsStoreProvider(); 43 | -------------------------------------------------------------------------------- /src/common/domain/zeplinComponent/model/BarrelDetails.ts: -------------------------------------------------------------------------------- 1 | import Barrel from "../../barrel/Barrel"; 2 | import ZeplinComponent from "./ZeplinComponent"; 3 | import ZeplinComponentSection from "./ZeplinComponentSection"; 4 | import ResponseScreenSection from "../../screen/model/ResponseScreenSection"; 5 | 6 | export default interface BarrelDetails extends Barrel { 7 | parentId?: string; 8 | description?: string; 9 | components: ZeplinComponent[]; 10 | componentSections: ZeplinComponentSection[]; 11 | screenSections?: ResponseScreenSection[]; 12 | } 13 | -------------------------------------------------------------------------------- /src/common/domain/zeplinComponent/model/BarrelError.ts: -------------------------------------------------------------------------------- 1 | import BaseError from "../../error/BaseError"; 2 | import BarrelType from "../../barrel/BarrelType"; 3 | 4 | export default class BarrelError extends BaseError { 5 | public constructor( 6 | public type: BarrelType, 7 | public id: string, 8 | message?: string, 9 | code?: number 10 | ) { 11 | super(message, code); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/common/domain/zeplinComponent/model/ResponseZeplinComponent.ts: -------------------------------------------------------------------------------- 1 | import ComponentLike from "../../componentLike/model/ComponentLike"; 2 | 3 | export default interface ResponseZeplinComponent extends ComponentLike { 4 | } 5 | -------------------------------------------------------------------------------- /src/common/domain/zeplinComponent/model/ZeplinComponent.ts: -------------------------------------------------------------------------------- 1 | import ResponseZeplinComponent from "./ResponseZeplinComponent"; 2 | import BarrelType from "../../barrel/BarrelType"; 3 | 4 | export default interface ZeplinComponent extends ResponseZeplinComponent { 5 | barrelId: string; 6 | barrelType: BarrelType; 7 | barrelName: string; 8 | sectionIds: string[]; 9 | sectionNames: string[]; 10 | } 11 | -------------------------------------------------------------------------------- /src/common/domain/zeplinComponent/model/ZeplinComponentDescriptors.ts: -------------------------------------------------------------------------------- 1 | export default interface ZeplinComponentDescriptors { 2 | zeplinIds?: string[]; 3 | zeplinNames?: string[]; 4 | } 5 | -------------------------------------------------------------------------------- /src/common/domain/zeplinComponent/model/ZeplinComponentSection.ts: -------------------------------------------------------------------------------- 1 | import ResponseZeplinComponent from "./ResponseZeplinComponent"; 2 | 3 | export default interface ZeplinComponentSection { 4 | _id: string; 5 | name: string; 6 | description?: string; 7 | components: ResponseZeplinComponent[]; 8 | componentSections: ZeplinComponentSection[]; 9 | } 10 | -------------------------------------------------------------------------------- /src/common/domain/zeplinComponent/util/zeplinComponentUi.ts: -------------------------------------------------------------------------------- 1 | import ZeplinComponent from "../model/ZeplinComponent"; 2 | import localization from "../../../../localization"; 3 | 4 | const DETAIL_ITEM_SEPARATOR = " ▸ "; 5 | 6 | function getZeplinComponentDetailRepresentation(zeplinComponent: ZeplinComponent, showLabel = false): string { 7 | return [ 8 | zeplinComponent.barrelName, 9 | showLabel ? localization.common.zeplinComponent.zeplinComponents : undefined, 10 | ...zeplinComponent.sectionNames 11 | ] 12 | .filter(representation => representation) 13 | .join(DETAIL_ITEM_SEPARATOR); 14 | } 15 | 16 | export { 17 | getZeplinComponentDetailRepresentation 18 | }; 19 | -------------------------------------------------------------------------------- /src/common/domain/zeplinComponent/util/zeplinComponentUtil.ts: -------------------------------------------------------------------------------- 1 | const ZEPLIN_COMPONENT_ID_VALIDATION_REGEX = /^[0-9A-Fa-f]{24}$/; 2 | 3 | function isZeplinComponentIdFormatValid(zeplinComponentId: string): boolean { 4 | return ZEPLIN_COMPONENT_ID_VALIDATION_REGEX.test(zeplinComponentId); 5 | } 6 | 7 | export { 8 | isZeplinComponentIdFormatValid 9 | }; 10 | -------------------------------------------------------------------------------- /src/common/general/booleanUtil.ts: -------------------------------------------------------------------------------- 1 | function isTrue(value: unknown): boolean { 2 | return typeof value === "boolean" ? value : false; 3 | } 4 | 5 | export { 6 | isTrue 7 | }; 8 | -------------------------------------------------------------------------------- /src/common/general/dateTimeUtil.ts: -------------------------------------------------------------------------------- 1 | const DAYS_IN_WEEK = 7; 2 | const MS_IN_SECOND = 1000; 3 | const SECONDS_IN_MINUTE = 60; 4 | const MS_IN_MINUTE = MS_IN_SECOND * SECONDS_IN_MINUTE; 5 | const MINUTES_IN_HOUR = 60; 6 | const MS_IN_HOUR = MINUTES_IN_HOUR * MS_IN_MINUTE; 7 | const HOURS_IN_DAY = 24; 8 | const MS_IN_DAY = HOURS_IN_DAY * MS_IN_HOUR; 9 | const DAYS_IN_MONTH = 30; 10 | const MS_IN_MONTH = DAYS_IN_MONTH * MS_IN_DAY; 11 | 12 | /** 13 | * Returns index of monday-based day 14 | */ 15 | function getWeekday(date: Date) { 16 | return (date.getDay() - 1) % DAYS_IN_WEEK; 17 | } 18 | 19 | export { 20 | DAYS_IN_WEEK, 21 | MS_IN_MINUTE, 22 | MS_IN_HOUR, 23 | MS_IN_DAY, 24 | MS_IN_MONTH, 25 | getWeekday 26 | }; 27 | -------------------------------------------------------------------------------- /src/common/general/iconPathUtil.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import * as path from "path"; 3 | import ContextProvider from "../vscode/extension/ContextProvider"; 4 | 5 | const LIGHT_RESOURCES_FOLDER_PATH = "resources/light"; 6 | const DARK_RESOURCES_FOLDER_PATH = "resources/dark"; 7 | 8 | enum IconTheme { 9 | Light, 10 | Dark 11 | } 12 | 13 | type ThemedIconUris = { 14 | light: vscode.Uri; 15 | dark: vscode.Uri; 16 | }; 17 | 18 | function getIconPath(fileName: string, theme: IconTheme): string { 19 | const folderPath = theme === IconTheme.Light ? LIGHT_RESOURCES_FOLDER_PATH : DARK_RESOURCES_FOLDER_PATH; 20 | 21 | return ContextProvider.get().asAbsolutePath(path.join(folderPath, fileName)); 22 | } 23 | 24 | function getIconUri(fileName: string, theme: IconTheme): vscode.Uri { 25 | return vscode.Uri.file(getIconPath(fileName, theme)); 26 | } 27 | 28 | function getThemedIconUris(fileName: string): ThemedIconUris { 29 | return { 30 | light: getIconUri(fileName, IconTheme.Light), 31 | dark: getIconUri(fileName, IconTheme.Dark) 32 | }; 33 | } 34 | 35 | export { 36 | IconTheme, 37 | getIconPath, 38 | getIconUri, 39 | getThemedIconUris 40 | }; 41 | -------------------------------------------------------------------------------- /src/common/general/imageUtil.ts: -------------------------------------------------------------------------------- 1 | import fetch from "node-fetch"; 2 | import AbortController from "abort-controller"; 3 | import imageSize from "image-size"; 4 | 5 | /** 6 | * Return width and height of an image by retrieving its data until metadata is received. 7 | * @param url An image url. 8 | */ 9 | async function getImageSize(url: string): Promise<{ width: number; height: number }> { 10 | const abortController = new AbortController(); 11 | let imageDataBuffer: Buffer | undefined; 12 | const response = await fetch(url, { signal: abortController.signal }); 13 | return new Promise(resolve => { 14 | response.body.on("data", (buffer: Buffer) => { 15 | imageDataBuffer = imageDataBuffer ? Buffer.concat([imageDataBuffer, buffer]) : buffer; 16 | try { 17 | const size = imageSize(imageDataBuffer); 18 | resolve(size); 19 | abortController.abort(); 20 | } catch { 21 | // Do nothing, continue receiving data 22 | } 23 | }); 24 | response.body.on("finish", () => resolve({ width: 0, height: 0 })); 25 | response.body.on("error", () => resolve({ width: 0, height: 0 })); 26 | }); 27 | } 28 | 29 | export { 30 | getImageSize 31 | }; 32 | -------------------------------------------------------------------------------- /src/common/general/pathUtil.ts: -------------------------------------------------------------------------------- 1 | import { unknownToLowercaseStringArray } from "./arrayUtil"; 2 | import { replaceAll } from "./stringUtil"; 3 | 4 | /** 5 | * Unix path separator. 6 | */ 7 | const PATH_SEPARATOR = "/"; 8 | /** 9 | * Windows path separator. 10 | */ 11 | const PATH_SEPARATOR_WINDOWS = "\\"; 12 | /** 13 | * File extension separator. 14 | */ 15 | const EXTENSION_SEPARATOR = "."; 16 | 17 | /** 18 | * Converts a path to Unix path. 19 | * @param path A path. 20 | */ 21 | function toUnixPath(path: string): string { 22 | return replaceAll(path, PATH_SEPARATOR_WINDOWS, PATH_SEPARATOR); 23 | } 24 | 25 | /** 26 | * Removes a path's first character if it is Unix or Windows path separator. 27 | * @param path A path. 28 | */ 29 | function removeLeadingPathSeparator(path: string): string { 30 | return path.length && (path.startsWith(PATH_SEPARATOR) || path.startsWith(PATH_SEPARATOR_WINDOWS)) 31 | ? path.substring(1) 32 | : path; 33 | } 34 | 35 | /** 36 | * Turns path(s) object to lowercase and unix path array. 37 | * @param paths Path(s) to be normalized. 38 | */ 39 | function normalizePaths(paths: unknown): string[] { 40 | return unknownToLowercaseStringArray(paths).map(toUnixPath); 41 | } 42 | 43 | export { 44 | PATH_SEPARATOR, 45 | EXTENSION_SEPARATOR, 46 | toUnixPath, 47 | removeLeadingPathSeparator, 48 | normalizePaths 49 | }; 50 | -------------------------------------------------------------------------------- /src/common/general/promiseUtil.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns a Promise that is 3 | * - resolved when original promise is rejected. 4 | * - rejected when original promise is resolved. 5 | * @param {Promise} promise A Promise to be reversed. 6 | */ 7 | async function reverse(promise: Promise): Promise { 8 | let result; 9 | try { 10 | result = await promise; 11 | } catch (error) { 12 | return error; 13 | } 14 | throw result; 15 | } 16 | 17 | /** 18 | * Returns a Promise that is 19 | * - resolved when any of the provided Promises are resolved. 20 | * - rejects when all of the provided Promises are rejected. 21 | * @param {Promise} promises An array of Promises. 22 | */ 23 | function promiseAny(promises: Promise[]): Promise { 24 | /* 25 | All promises are reversed before being supplied to Promise.all method. Thus, Promise.all will be: 26 | - resolved when all promises are rejected. 27 | - rejected when one of promises is resolved. (Will not wait all promises to settle because a rejection make 28 | Promise.all to reject.) 29 | 30 | Then, the result is reversed again and resulting promise will be: 31 | - rejected when all promises are rejected. 32 | - resolved when one of promises is resolved. 33 | */ 34 | return reverse(Promise.all(promises.map(reverse))) as Promise; 35 | } 36 | 37 | export { 38 | promiseAny 39 | }; 40 | -------------------------------------------------------------------------------- /src/common/general/stringUtil.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Replaces all occurrences of a value in a string with another value. 3 | * @param originalString A string which search value is searched on. 4 | * @param searchValue A value to search in the original string. 5 | * @param replaceValue A value to replace the search value with. 6 | */ 7 | function replaceAll(originalString: string, searchValue: string, replaceValue: string): string { 8 | return originalString.split(searchValue).join(replaceValue); 9 | } 10 | 11 | /** 12 | * Return indices of all occurrences of a value in a string. 13 | * @param originalString A string which search string is searched on. 14 | * @param searchValue A value to search in original string. 15 | */ 16 | function allIndicesOf(originalString: string, searchValue: string): number[] { 17 | const searchLength = searchValue.length; 18 | if (searchLength === 0) { 19 | return []; 20 | } 21 | 22 | const foundIndices: number[] = []; 23 | let foundIndex; 24 | let searchIndex = 0; 25 | while ((foundIndex = originalString.indexOf(searchValue, searchIndex)) >= 0) { 26 | foundIndices.push(foundIndex); 27 | searchIndex = foundIndex + searchLength; 28 | } 29 | 30 | return foundIndices; 31 | } 32 | 33 | /** 34 | * Converts given snake_case string to PascalCase 35 | * @param str A string to be converted 36 | */ 37 | function snakeCaseToPascalCase(str: string): string { 38 | return uppercaseFirst(str.replace(/_\w/g, word => word.charAt(1).toUpperCase())); 39 | } 40 | 41 | /** 42 | * Returns given string as its first character uppercased 43 | * @param str A string to be converted 44 | */ 45 | function uppercaseFirst(str: string) { 46 | return !str.length ? str : str.charAt(0).toUpperCase() + str.substring(1); 47 | } 48 | 49 | function compareLength(str1: string, str2: string): number { 50 | return str2.length - str1.length; 51 | } 52 | 53 | export { 54 | replaceAll, 55 | allIndicesOf, 56 | snakeCaseToPascalCase, 57 | compareLength 58 | }; 59 | -------------------------------------------------------------------------------- /src/common/vscode/codeLens/CodeLensCreator.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | /** 4 | * A Code Lens creator to provide to CodeLensProvider. 5 | */ 6 | export default interface CodeLensCreator { 7 | /** 8 | * Creates Code Lens on a text document. 9 | * @param document A text document to create Code Lens on. 10 | */ 11 | create(document: vscode.TextDocument): vscode.CodeLens[]; 12 | } 13 | -------------------------------------------------------------------------------- /src/common/vscode/codeLens/CodeLensProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import CodeLensCreator from "./CodeLensCreator"; 3 | import { wrapWithLogs } from "../../../log/util/logUtil"; 4 | import { flatten, isFirstOccurence } from "../../general/arrayUtil"; 5 | import Logger from "../../../log/Logger"; 6 | 7 | /** 8 | * A code lens provider adds commands to source text. The commands will be shown as dedicated horizontal lines in 9 | * between the source text. 10 | */ 11 | export default abstract class CodeLensProvider implements vscode.CodeLensProvider { 12 | private eventEmitter = new vscode.EventEmitter(); 13 | 14 | public get onDidChangeCodeLenses(): vscode.Event { 15 | return this.eventEmitter.event; 16 | } 17 | 18 | /** 19 | * Re-provides code lenses. 20 | */ 21 | public refresh() { 22 | this.eventEmitter.fire(); 23 | } 24 | 25 | /** 26 | * Returns a selector that defines the documents this provider is applicable to. 27 | */ 28 | public abstract getDocumentSelector(): vscode.DocumentSelector; 29 | 30 | /** 31 | * Returns Code Lens creators that this provider provides. 32 | */ 33 | protected abstract getCodeLensCreators(document: vscode.TextDocument): CodeLensCreator[]; 34 | 35 | /** 36 | * Returns Code Lenses that are this provider create via its Code Lens creators. 37 | * @param document A text document to create Code Lenses on. 38 | */ 39 | public provideCodeLenses(document: vscode.TextDocument): vscode.CodeLens[] { 40 | const codeLensArrays: vscode.CodeLens[][] = this.getCodeLensCreators(document).map( 41 | creator => wrapWithLogs( 42 | () => creator.create(document), 43 | `Code Lens "${creator.constructor.name}"`, 44 | true 45 | ) 46 | ); 47 | const codeLenses = flatten(codeLensArrays); 48 | const distinctCommandTitles = codeLenses 49 | .filter(codeLens => codeLens.command) 50 | .map(codeLens => codeLens.command!.title) 51 | .filter(isFirstOccurence); 52 | if (distinctCommandTitles.length) { 53 | Logger.log(`Shown Code Lenses: ${distinctCommandTitles.join(", ")}`); 54 | } 55 | return codeLenses; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/common/vscode/codeLens/codeLensUtil.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | /** 4 | * Returns a Code Lens with a position and a command. 5 | * @param position A position to create Code Lens on. 6 | * @param command A command to assign to Code Lens. 7 | */ 8 | function createCodeLens(position: vscode.Position, command: vscode.Command): vscode.CodeLens { 9 | return new vscode.CodeLens(new vscode.Range(position, position), command); 10 | } 11 | 12 | /** 13 | * Returns Code Lenses with given positions and a command. 14 | * @param positions Positions to create Code Lenses on. 15 | * @param command A command to assign to Code Lenses. 16 | */ 17 | function createCodeLenses(positions: vscode.Position[], command: vscode.Command): vscode.CodeLens[] { 18 | return positions.map(position => createCodeLens(position, command)); 19 | } 20 | 21 | export { 22 | createCodeLens, 23 | createCodeLenses 24 | }; 25 | -------------------------------------------------------------------------------- /src/common/vscode/command/Command.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Descriptor of a Command with name and action. 3 | */ 4 | export default interface Command { 5 | /** 6 | * Name of the command, must be same as the command parameter in package.json. 7 | */ 8 | name: string; 9 | 10 | /** 11 | * Executes action of this command with parameters. 12 | * @param args Parameters of action. 13 | */ 14 | execute(...args: unknown[]): Promise; 15 | } 16 | -------------------------------------------------------------------------------- /src/common/vscode/diagnostic/DiagnosticCreator.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | /** 4 | * A diagnostic creator to provide to Diagnostic providers. 5 | */ 6 | export default interface DiagnosticCreator { 7 | /** 8 | * Creates diagnostics on a text document. 9 | * @param document A text document to create diagnostics on. 10 | */ 11 | create(document: vscode.TextDocument): vscode.ProviderResult; 12 | } 13 | -------------------------------------------------------------------------------- /src/common/vscode/diagnostic/diagnosticsUtil.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { flatten } from "../../general/arrayUtil"; 3 | import { getRangesOfProperty } from "../editor/textDocumentUtil"; 4 | 5 | /** 6 | * Returns Diagnostics on a text document with given property and a message. 7 | * @param propertyName Property name to search for on given text document. 8 | * @param message Message of diagnostics to be created. 9 | * @param document A text document to create diagnostics on. 10 | * @param severity Severity of diagnostics to be created 11 | */ 12 | function createDiagnostics( 13 | propertyName: string, 14 | message: string, 15 | document: vscode.TextDocument, 16 | severity?: vscode.DiagnosticSeverity 17 | ): vscode.Diagnostic[]; 18 | 19 | /** 20 | * Returns Diagnostics on a text document with given properties and a message. 21 | * @param propertyNames Property names to search for on given text document. 22 | * @param message Message of diagnostics to be created. 23 | * @param document A text document to create diagnostics on. 24 | * @param severity Severity of diagnostics to be created 25 | */ 26 | function createDiagnostics( 27 | propertyNames: string[], 28 | message: string, 29 | document: vscode.TextDocument, 30 | severity?: vscode.DiagnosticSeverity 31 | ): vscode.Diagnostic[]; 32 | 33 | function createDiagnostics( 34 | propertyNameOrNames: string | string[], 35 | message: string, 36 | document: vscode.TextDocument, 37 | severity = vscode.DiagnosticSeverity.Error 38 | ): vscode.Diagnostic[] { 39 | const propertyNames = typeof propertyNameOrNames === "string" ? [propertyNameOrNames] : propertyNameOrNames; 40 | 41 | return flatten( 42 | propertyNames.map( 43 | property => getRangesOfProperty(property, document).map( 44 | range => new vscode.Diagnostic(range, message, severity) 45 | ) 46 | ) 47 | ); 48 | } 49 | 50 | export { 51 | createDiagnostics 52 | }; 53 | -------------------------------------------------------------------------------- /src/common/vscode/extension/ContextProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | /** 4 | * Provides context to use throughout the extension. 5 | */ 6 | class ContextProvider { 7 | private context?: vscode.ExtensionContext; 8 | 9 | /** 10 | * Sets context to provide later. 11 | * @param context Context. 12 | */ 13 | public initialize(context: vscode.ExtensionContext) { 14 | this.context = context; 15 | } 16 | 17 | /** 18 | * Returns context. 19 | */ 20 | public get(): vscode.ExtensionContext { 21 | return this.context!; 22 | } 23 | } 24 | 25 | export default new ContextProvider(); 26 | -------------------------------------------------------------------------------- /src/common/vscode/extension/extensionUtil.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import Command from "../command/Command"; 3 | import { wrapWithLogsAsync } from "../../../log/util/logUtil"; 4 | import ContextProvider from "./ContextProvider"; 5 | 6 | /** 7 | * Name of the publisher. 8 | */ 9 | const EXTENSION_PUBLISHER = "Zeplin"; 10 | /** 11 | * Name of this extension. 12 | */ 13 | const EXTENSION_NAME = "zeplin"; 14 | /** 15 | * Id of this extension. 16 | */ 17 | const EXTENSION_IDENTIFIER = `${EXTENSION_PUBLISHER}.${EXTENSION_NAME}`; 18 | const KEY_INITIALIZED = "initialized"; 19 | 20 | /** 21 | * Returns version of this extension. 22 | */ 23 | function getExtensionVersion(): string { 24 | return vscode.extensions.getExtension(EXTENSION_IDENTIFIER)!.packageJSON.version; 25 | } 26 | 27 | /** 28 | * Checks whether this extension is initialized. 29 | */ 30 | function isInitialized(): boolean { 31 | return ContextProvider.get().globalState.get(KEY_INITIALIZED) ?? false; 32 | } 33 | 34 | /** 35 | * Sets this extension is initialized. 36 | */ 37 | function initialized() { 38 | return ContextProvider.get().globalState.update(KEY_INITIALIZED, true); 39 | } 40 | 41 | /** 42 | * Registers command with wrapping with logs. 43 | * @param command Command 44 | */ 45 | function registerCommand(command: Command) { 46 | ContextProvider.get().subscriptions.push( 47 | vscode.commands.registerCommand( 48 | command.name, 49 | (...args) => wrapWithLogsAsync( 50 | () => command.execute(...args), 51 | `Command "${command.name}"` 52 | ) 53 | ) 54 | ); 55 | } 56 | 57 | export { 58 | EXTENSION_NAME, 59 | getExtensionVersion, 60 | isInitialized as isExtensionInitialized, 61 | initialized as extensionInitialized, 62 | registerCommand 63 | }; 64 | -------------------------------------------------------------------------------- /src/common/vscode/fileChange/FileChanges.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | export default interface FileChanges { 4 | oldUri: vscode.Uri; 5 | newUri: vscode.Uri; 6 | } 7 | -------------------------------------------------------------------------------- /src/common/vscode/ide/builtinCommands.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | const COMMAND_NAME_SET_CONTEXT = "setContext"; 4 | 5 | function setContext(key: string, value: unknown) { 6 | vscode.commands.executeCommand(COMMAND_NAME_SET_CONTEXT, key, value); 7 | } 8 | 9 | export { 10 | setContext 11 | }; 12 | -------------------------------------------------------------------------------- /src/common/vscode/ide/vscodeFeatureVersions.ts: -------------------------------------------------------------------------------- 1 | 2 | const HOVER_CODICONS_MIN_VERSION = "1.42"; 3 | const HOVER_CODICONS_BROKEN_VERSION = "1.46.0"; 4 | const VIEWS_WELCOME_MIN_VERSION = "1.44"; 5 | 6 | export { 7 | HOVER_CODICONS_MIN_VERSION, 8 | HOVER_CODICONS_BROKEN_VERSION, 9 | VIEWS_WELCOME_MIN_VERSION 10 | }; 11 | -------------------------------------------------------------------------------- /src/common/vscode/ide/vscodeUtil.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | const VERSION_SEPARATOR = "."; 4 | 5 | function isVscodeVersionSufficient(requiredVersion: string): boolean { 6 | const splittedRequiredVersion = requiredVersion.split(VERSION_SEPARATOR).map(str => Number.parseInt(str, 10)); 7 | const splittedCurrentVersion = vscode.version.split(VERSION_SEPARATOR).map(str => Number.parseInt(str, 10)); 8 | 9 | for (let digitIndex = 0; digitIndex < splittedRequiredVersion.length; digitIndex++) { 10 | if (splittedCurrentVersion.length <= digitIndex) { 11 | return true; 12 | } else if (splittedCurrentVersion[digitIndex] < splittedRequiredVersion[digitIndex]) { 13 | return false; 14 | } else if (splittedCurrentVersion[digitIndex] > splittedRequiredVersion[digitIndex]) { 15 | return true; 16 | } 17 | } 18 | 19 | return splittedCurrentVersion.length >= splittedRequiredVersion.length; 20 | } 21 | 22 | function isVscodeVersionEqualTo(version: string) { 23 | return vscode.version === version; 24 | } 25 | 26 | export { 27 | isVscodeVersionSufficient, 28 | isVscodeVersionEqualTo 29 | }; 30 | -------------------------------------------------------------------------------- /src/common/vscode/message/MessageType.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Type of VS Code Message. 3 | */ 4 | enum MessageType { 5 | /** 6 | * Show message as information. 7 | */ 8 | Info = "Info", 9 | /** 10 | * Show message as warning. 11 | */ 12 | Warning = "Warning", 13 | /** 14 | * Show message as error. 15 | */ 16 | Error = "Error", 17 | } 18 | 19 | export default MessageType; 20 | -------------------------------------------------------------------------------- /src/common/vscode/tree/ExpandedErrorTreeItem.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import TreeItem from "./TreeItem"; 3 | 4 | export default class ExpandedErrorTreeItem extends TreeItem { 5 | public constructor(title: string, private message: string, parent: TreeItem | undefined) { 6 | super(title, parent, undefined, vscode.TreeItemCollapsibleState.Expanded); 7 | } 8 | 9 | public getChildren(): TreeItem[] { 10 | return [new TreeItem(this.message, this)]; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/common/vscode/tree/TreeDataProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import TreeItem from "./TreeItem"; 3 | import Logger from "../../../log/Logger"; 4 | 5 | export default abstract class TreeDataProvider implements vscode.TreeDataProvider { 6 | protected treeView?: vscode.TreeView; 7 | protected abstract viewId: string; 8 | protected showCollapseAll = false; 9 | private eventEmitter = new vscode.EventEmitter(); 10 | 11 | public get onDidChangeTreeData(): vscode.Event { 12 | return this.eventEmitter.event; 13 | } 14 | 15 | public refresh() { 16 | this.eventEmitter.fire(); 17 | } 18 | 19 | public refreshItem(item?: TreeItem) { 20 | this.eventEmitter.fire(item); 21 | } 22 | 23 | public getTreeItem(item: TreeItem): TreeItem { 24 | return item; 25 | } 26 | 27 | public getChildren(element?: TreeItem | undefined): vscode.ProviderResult { 28 | return element ? element.getChildren() : this.getRoots(); 29 | } 30 | 31 | public register(): vscode.Disposable { 32 | this.treeView = vscode.window.createTreeView(this.viewId, { 33 | treeDataProvider: this, 34 | showCollapseAll: this.showCollapseAll 35 | }); 36 | const visibilityChangeLogger = this.treeView!.onDidChangeVisibility(({ visible }) => { 37 | Logger.log(`Tree "${this.viewId}" visibility changed: ${visible}`); 38 | }); 39 | 40 | return vscode.Disposable.from(this.treeView, visibilityChangeLogger); 41 | } 42 | 43 | public abstract getRoots(): vscode.ProviderResult; 44 | 45 | public getParent(element: TreeItem): TreeItem | undefined { 46 | return element.getParent(); 47 | } 48 | 49 | protected reveal(treeItem: TreeItem | undefined, itemDescription: string) { 50 | if (treeItem) { 51 | this.treeView?.reveal(treeItem); 52 | } else { 53 | Logger.log(`${itemDescription} could not be located on ${this.viewId}`); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/common/vscode/tree/TreeItem.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import TreeItemContextProvider from "./TreeItemContextProvider"; 3 | import { getCroppedImageUrl } from "../../domain/image/zeplinImageUtil"; 4 | import { refreshItem } from "../../../sidebar/refresh/util/refreshUtil"; 5 | import TreeItemContext from "../../domain/tree/TreeItemContext"; 6 | import OpenInZeplinOnDoubleClickCommand from "../../../sidebar/openInZeplin/command/OpenInZeplinOnDoubleClickCommand"; 7 | 8 | const ICON_SIZE = 32; 9 | 10 | export default class TreeItem extends vscode.TreeItem { 11 | public constructor( 12 | label: string, 13 | private parent: TreeItem | undefined, 14 | private contextProvider?: TreeItemContextProvider, 15 | collapsibleState?: vscode.TreeItemCollapsibleState 16 | ) { 17 | super(label, collapsibleState); 18 | this.contextValue = this.contextProvider?.get(); 19 | if (contextProvider?.contains(TreeItemContext.ZeplinLink)) { 20 | this.command = { 21 | command: OpenInZeplinOnDoubleClickCommand.name, 22 | title: "", 23 | arguments: [this] 24 | }; 25 | } 26 | } 27 | 28 | public containsContext(contextValue: string): boolean { 29 | return this.contextProvider?.contains(contextValue) ?? false; 30 | } 31 | 32 | public getChildren(): vscode.ProviderResult { 33 | return []; 34 | } 35 | 36 | public getParent(): TreeItem | undefined { 37 | return this.parent; 38 | } 39 | 40 | protected async setRemoteIconPath(path: string | undefined) { 41 | if (path) { 42 | const croppedImageUrl = await getCroppedImageUrl(path, ICON_SIZE, ICON_SIZE); 43 | this.iconPath = croppedImageUrl ? vscode.Uri.parse(croppedImageUrl) : undefined; 44 | 45 | refreshItem(this); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/common/vscode/tree/TreeItemContextProvider.ts: -------------------------------------------------------------------------------- 1 | const WORD_SEPARATOR = "/"; 2 | 3 | export default class TreeItemContextProvider { 4 | private readonly contextValues: string[]; 5 | private readonly context: string; 6 | 7 | public constructor(...contextValues: string[]) { 8 | this.contextValues = contextValues; 9 | this.context = contextValues.join(WORD_SEPARATOR); 10 | } 11 | 12 | public contains(contextValue: string): boolean { 13 | return this.contextValues.includes(contextValue); 14 | } 15 | 16 | public get(): string { 17 | return this.context; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/common/vscode/uri/uriUtil.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | /** 4 | * Determines whether a text is a URI. 5 | * @param text A text to be checked. 6 | */ 7 | function isUri(text: string): boolean { 8 | try { 9 | vscode.Uri.parse(text, true); 10 | return true; 11 | } catch (exception) { 12 | return false; 13 | } 14 | } 15 | 16 | export { 17 | isUri 18 | }; 19 | -------------------------------------------------------------------------------- /src/localization/index.ts: -------------------------------------------------------------------------------- 1 | import localization from "./localization"; 2 | 3 | export default localization; 4 | -------------------------------------------------------------------------------- /src/log/command/SaveLogsCommand.ts: -------------------------------------------------------------------------------- 1 | import Command from "../../common/vscode/command/Command"; 2 | import Logger from "../Logger"; 3 | 4 | class SaveLogsCommand implements Command { 5 | public name = "zeplin.saveLogs"; 6 | 7 | public execute() { 8 | Logger.saveToFile(); 9 | return Promise.resolve(); 10 | } 11 | } 12 | 13 | export default new SaveLogsCommand(); 14 | -------------------------------------------------------------------------------- /src/log/util/logUtil.ts: -------------------------------------------------------------------------------- 1 | import Logger from "../Logger"; 2 | 3 | const NONSEQUENTIAL_OPERATION_MARKER = "################"; 4 | 5 | function wrapWithLogs(method: () => T, logMarker: string, onlyExceptions = false): T { 6 | try { 7 | log(`${logMarker} started`, onlyExceptions); 8 | const result = method(); 9 | log(`${logMarker} finished`, onlyExceptions); 10 | return result; 11 | } catch (error) { 12 | logError(logMarker, error); 13 | throw error; 14 | } 15 | } 16 | 17 | async function wrapWithLogsAsync(method: () => Promise, logMarker: string, onlyExceptions = false): Promise { 18 | try { 19 | log(`${logMarker} started`, onlyExceptions); 20 | const result = await method(); 21 | log(`${logMarker} finished`, onlyExceptions); 22 | return result; 23 | } catch (error) { 24 | logError(logMarker, error); 25 | throw error; 26 | } 27 | } 28 | 29 | function log(message: string, skip = false) { 30 | if (!skip) { 31 | Logger.log(`${NONSEQUENTIAL_OPERATION_MARKER} ${message}`); 32 | } 33 | } 34 | 35 | function logError(message: string, error: Error) { 36 | Logger.error(`${NONSEQUENTIAL_OPERATION_MARKER} ${message} failed`, error); 37 | } 38 | 39 | export { 40 | wrapWithLogs, 41 | wrapWithLogsAsync 42 | }; 43 | -------------------------------------------------------------------------------- /src/session/Session.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as vscode from "vscode"; 3 | import { TokenStorage } from "./tokenStorage"; 4 | import ContextProvider from "../common/vscode/extension/ContextProvider"; 5 | import { setContext } from "../common/vscode/ide/builtinCommands"; 6 | 7 | const STATE_KEY_LOGGED_IN = "zeplinLoggedIn"; 8 | const CONTEXT_KEY_LOGGED_IN = "zeplin:loggedIn"; 9 | 10 | /** 11 | * Session data storage wrapper. 12 | */ 13 | class Session { 14 | private memento?: vscode.Memento; 15 | private tokenStorage?: TokenStorage; 16 | 17 | /** 18 | * Initializes session data with a token storage. 19 | * @param tokenStorage An available token storage mechanism. 20 | */ 21 | public initialize(tokenStorage: TokenStorage) { 22 | this.memento = ContextProvider.get().globalState; 23 | this.tokenStorage = tokenStorage; 24 | setContext(CONTEXT_KEY_LOGGED_IN, this.isLoggedIn()); 25 | } 26 | 27 | /** 28 | * Determines whether user is logged in. 29 | */ 30 | public isLoggedIn(): boolean { 31 | return this.memento!.get(STATE_KEY_LOGGED_IN) ?? false; 32 | } 33 | 34 | /** 35 | * Returns session token. 36 | */ 37 | public getToken(): Promise { 38 | return this.tokenStorage!.get(); 39 | } 40 | 41 | /** 42 | * Saves session token. 43 | * @param token A session token to save. 44 | */ 45 | public setToken(token: string) { 46 | this.memento!.update(STATE_KEY_LOGGED_IN, true); 47 | setContext(CONTEXT_KEY_LOGGED_IN, true); 48 | return this.tokenStorage!.set(token); 49 | } 50 | 51 | /** 52 | * Removes session token. 53 | */ 54 | public removeToken() { 55 | this.memento!.update(STATE_KEY_LOGGED_IN, false); 56 | setContext(CONTEXT_KEY_LOGGED_IN, false); 57 | return this.tokenStorage!.remove(); 58 | } 59 | } 60 | 61 | export default new Session(); 62 | -------------------------------------------------------------------------------- /src/session/codeLens/ClearCacheCodeLensCreator.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import CodeLensCreator from "../../common/vscode/codeLens/CodeLensCreator"; 3 | import localization from "../../localization/localization"; 4 | import ClearCacheCommand from "../command/ClearCacheCommand"; 5 | import Session from "../Session"; 6 | 7 | class ClearCacheCodeLensCreator implements CodeLensCreator { 8 | public create(): vscode.CodeLens[] { 9 | if (!Session.isLoggedIn()) { 10 | return []; 11 | } else { 12 | return [new vscode.CodeLens( 13 | new vscode.Range(0, 0, 0, 0), 14 | { 15 | command: ClearCacheCommand.name, 16 | title: localization.session.clearCache 17 | } 18 | )]; 19 | } 20 | } 21 | } 22 | 23 | export default new ClearCacheCodeLensCreator(); 24 | -------------------------------------------------------------------------------- /src/session/codeLens/LoginCodeLensCreator.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import CodeLensCreator from "../../common/vscode/codeLens/CodeLensCreator"; 3 | import Session from "../Session"; 4 | import LoginCommand from "../command/LoginCommand"; 5 | import localization from "../../localization/localization"; 6 | 7 | class LoginCodeLensCreator implements CodeLensCreator { 8 | public create(): vscode.CodeLens[] { 9 | if (Session.isLoggedIn()) { 10 | return []; 11 | } else { 12 | return [new vscode.CodeLens( 13 | new vscode.Range(0, 0, 0, 0), 14 | { 15 | command: LoginCommand.name, 16 | title: localization.session.loginToZeplin 17 | } 18 | )]; 19 | } 20 | } 21 | } 22 | 23 | export default new LoginCodeLensCreator(); 24 | -------------------------------------------------------------------------------- /src/session/command/ClearCacheCommand.ts: -------------------------------------------------------------------------------- 1 | import Command from "../../common/vscode/command/Command"; 2 | import { clearCacheAndNotify } from "../flow/sessionFlow"; 3 | 4 | class ClearCacheCommand implements Command { 5 | public name = "zeplin.clearCache"; 6 | 7 | public execute() { 8 | clearCacheAndNotify(); 9 | return Promise.resolve(); 10 | } 11 | } 12 | 13 | export default new ClearCacheCommand(); 14 | -------------------------------------------------------------------------------- /src/session/command/LoginCommand.ts: -------------------------------------------------------------------------------- 1 | import Command from "../../common/vscode/command/Command"; 2 | import { tryLogin } from "../flow/sessionFlow"; 3 | 4 | class LoginCommand implements Command { 5 | public name = "zeplin.login"; 6 | 7 | public execute() { 8 | tryLogin(); 9 | return Promise.resolve(); 10 | } 11 | } 12 | 13 | export default new LoginCommand(); 14 | -------------------------------------------------------------------------------- /src/session/command/LogoutCommand.ts: -------------------------------------------------------------------------------- 1 | import Command from "../../common/vscode/command/Command"; 2 | import { tryLogout } from "../flow/sessionFlow"; 3 | 4 | class LogoutCommand implements Command { 5 | public name = "zeplin.logout"; 6 | 7 | public execute() { 8 | tryLogout(); 9 | return Promise.resolve(); 10 | } 11 | } 12 | 13 | export default new LogoutCommand(); 14 | -------------------------------------------------------------------------------- /src/session/command/ManualLoginCommand.ts: -------------------------------------------------------------------------------- 1 | import Command from "../../common/vscode/command/Command"; 2 | import { tryManualLogin } from "../flow/sessionFlow"; 3 | 4 | class ManualLoginCommand implements Command { 5 | public name = "zeplin.manualLogin"; 6 | 7 | public execute() { 8 | return tryManualLogin(); 9 | } 10 | } 11 | 12 | export default new ManualLoginCommand(); 13 | -------------------------------------------------------------------------------- /src/session/tokenStorage/KeychainTokenStorage.ts: -------------------------------------------------------------------------------- 1 | import * as keytarType from "keytar"; 2 | import { TokenStorage } from "."; 3 | import Logger from "../../log/Logger"; 4 | 5 | const SERVICE_NAME = "zeplinTokenService"; 6 | const ACCOUNT_NAME = "zeplinTokenAccount"; 7 | const SERVICE_NAME_FOR_AVAILABILITY = "zeplinTokenServiceForAvailability"; 8 | const ACCOUNT_NAME_FOR_AVAILABILITY = "zeplinTokenAccountForAvailability"; 9 | 10 | /** 11 | * Native storage for session token. 12 | */ 13 | class KeychainTokenStorage implements TokenStorage { 14 | public get(): Promise { 15 | return keytar!.getPassword(SERVICE_NAME, ACCOUNT_NAME); 16 | } 17 | 18 | public set(token: string): Promise { 19 | return keytar!.setPassword(SERVICE_NAME, ACCOUNT_NAME, token); 20 | } 21 | 22 | public async remove(): Promise { 23 | await keytar!.deletePassword(SERVICE_NAME, ACCOUNT_NAME); 24 | } 25 | } 26 | 27 | type Keytar = { 28 | getPassword: typeof keytarType["getPassword"]; 29 | setPassword: typeof keytarType["setPassword"]; 30 | deletePassword: typeof keytarType["deletePassword"]; 31 | }; 32 | 33 | const keytar = getKeytar(); 34 | 35 | function getKeytar(): Keytar | undefined { 36 | try { 37 | const vscodeRequire = eval("require"); // eslint-disable-line no-eval 38 | return vscodeRequire("keytar"); 39 | } catch (err) { 40 | return undefined; 41 | } 42 | } 43 | 44 | /** 45 | * Determines whether native password storage could be used for session token. 46 | */ 47 | async function isKeychainTokenStorageAvailable() { 48 | try { 49 | // Check if keytar is working 50 | if (!keytar) { 51 | Logger.log("Keychain is not available"); 52 | return false; 53 | } 54 | await keytar.getPassword(SERVICE_NAME_FOR_AVAILABILITY, ACCOUNT_NAME_FOR_AVAILABILITY); 55 | Logger.log("Keychain is available"); 56 | return true; 57 | } catch (error) { 58 | Logger.error("Keychain is not available", error); 59 | return false; 60 | } 61 | } 62 | 63 | export { 64 | KeychainTokenStorage, 65 | isKeychainTokenStorageAvailable 66 | }; 67 | -------------------------------------------------------------------------------- /src/session/tokenStorage/TokenStorage.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Storage for session token. 3 | */ 4 | export default interface TokenStorage { 5 | get(): Promise; 6 | set(token: string): Promise; 7 | remove(): Promise; 8 | } 9 | -------------------------------------------------------------------------------- /src/session/tokenStorage/VscodeTokenStorage.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { TokenStorage } from "."; 3 | import ContextProvider from "../../common/vscode/extension/ContextProvider"; 4 | 5 | const KEY_TOKEN = "zeplinToken"; 6 | 7 | /** 8 | * VS Code storage for session token. 9 | */ 10 | export default class VscodeTokenStorage implements TokenStorage { 11 | private readonly memento: vscode.Memento; 12 | 13 | public constructor() { 14 | this.memento = ContextProvider.get().globalState; 15 | } 16 | 17 | public get(): Promise { 18 | return new Promise(resolve => resolve(this.memento.get(KEY_TOKEN))); 19 | } 20 | 21 | public set(token: string): Promise { 22 | return new Promise(resolve => resolve(this.memento.update(KEY_TOKEN, token))); 23 | } 24 | 25 | public remove(): Promise { 26 | return new Promise(resolve => resolve(this.memento.update(KEY_TOKEN, null))); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/session/tokenStorage/index.ts: -------------------------------------------------------------------------------- 1 | import { KeychainTokenStorage, isKeychainTokenStorageAvailable } from "./KeychainTokenStorage"; 2 | import VscodeTokenStorage from "./VscodeTokenStorage"; 3 | import TokenStorage from "./TokenStorage"; 4 | 5 | export { 6 | TokenStorage, 7 | KeychainTokenStorage, 8 | VscodeTokenStorage, 9 | isKeychainTokenStorageAvailable 10 | }; 11 | -------------------------------------------------------------------------------- /src/session/util/Refresher.ts: -------------------------------------------------------------------------------- 1 | import WorkspacesStore from "../../common/domain/barrel/data/WorkspacesStore"; 2 | import BarrelsStoreProvider from "../../common/domain/barrel/data/BarrelsStoreProvider"; 3 | import BarrelDetailsStoreProvider from "../../common/domain/zeplinComponent/data/BarrelDetailsStoreProvider"; 4 | import ScreensStoreProvider from "../../sidebar/screen/data/ScreensStoreProvider"; 5 | import BarrelTreeDataProvider from "../../sidebar/barrel/tree/BarrelTreeDataProvider"; 6 | import ActivityTreeDataProvider from "../../sidebar/activity/tree/ActivityTreeDataProvider"; 7 | import { MS_IN_HOUR } from "../../common/general/dateTimeUtil"; 8 | import { resetCroppedImageUrlCache } from "../../common/domain/image/zeplinImageUtil"; 9 | import ConfigDiagnosticsProvider from "../../coco/config/diagnostic/ConfigDiagnosticsProvider"; 10 | 11 | class Refresher { 12 | private lastRefreshRequested?: number; 13 | 14 | public refresh() { 15 | this.lastRefreshRequested = Date.now(); 16 | 17 | WorkspacesStore.clearCache(); 18 | BarrelsStoreProvider.clearCache(); 19 | BarrelDetailsStoreProvider.clearCache(); 20 | ScreensStoreProvider.clearCache(); 21 | resetCroppedImageUrlCache(); 22 | ConfigDiagnosticsProvider.updateForOpenDocuments(); 23 | BarrelTreeDataProvider.refresh(); 24 | ActivityTreeDataProvider.refresh(); 25 | } 26 | 27 | public requestRefresh() { 28 | if (this.refreshRequired()) { 29 | this.refresh(); 30 | } 31 | } 32 | 33 | /** 34 | * Returns true if data is not refreshed in the last hour. 35 | */ 36 | private refreshRequired(): boolean { 37 | return !this.lastRefreshRequested || this.lastRefreshRequested < Date.now() - MS_IN_HOUR; 38 | } 39 | } 40 | 41 | export default new Refresher(); 42 | -------------------------------------------------------------------------------- /src/session/util/SessionInitializer.ts: -------------------------------------------------------------------------------- 1 | import { isKeychainTokenStorageAvailable, KeychainTokenStorage, VscodeTokenStorage } from "../tokenStorage"; 2 | import Session from "../Session"; 3 | 4 | async function initializeSession() { 5 | Session.initialize(await isKeychainTokenStorageAvailable() ? new KeychainTokenStorage() : new VscodeTokenStorage()); 6 | } 7 | 8 | export { 9 | initializeSession 10 | }; 11 | -------------------------------------------------------------------------------- /src/sidebar/activity/data/ActivityStore.ts: -------------------------------------------------------------------------------- 1 | import Store from "../../../common/domain/store/Store"; 2 | import Result from "../../../common/domain/store/Result"; 3 | import ScreensStoreProvider from "../../screen/data/ScreensStoreProvider"; 4 | import BarrelDetailsStoreProvider from "../../../common/domain/zeplinComponent/data/BarrelDetailsStoreProvider"; 5 | import { getSavedBarrels } from "../../barrel/util/barrelUtil"; 6 | import BarrelType from "../../../common/domain/barrel/BarrelType"; 7 | import ZeplinComponentsStore from "../../../common/domain/zeplinComponent/data/ZeplinComponentsStore"; 8 | import Activity from "../model/Activity"; 9 | import { flatten } from "../../../common/general/arrayUtil"; 10 | import ScreenActivity from "../model/ScreenActivity"; 11 | import ComponentActivity from "../model/ComponentActivity"; 12 | 13 | class ActivityStore implements Store { 14 | public get = async (): Promise> => { 15 | const barrels = getSavedBarrels(); 16 | const projects = barrels.filter(barrel => barrel.type === BarrelType.Project); 17 | const results = await Promise.all([ 18 | ...projects.map(({ id }) => ScreensStoreProvider.get(id).get().then( 19 | result => ({ 20 | data: result.data?.map(screen => new ScreenActivity(screen, id)), 21 | errors: result.errors 22 | } as Result) 23 | )), 24 | ...barrels.map(({ id, type }) => new ZeplinComponentsStore(id, type).get().then( 25 | result => ({ 26 | data: result.data?.map(component => new ComponentActivity(component, id, type)), 27 | errors: result.errors 28 | } as Result) 29 | )) 30 | ]); 31 | 32 | return { 33 | data: flatten(results.map(result => result.data ?? [])), 34 | errors: flatten(results.map(result => result.errors ?? [])) 35 | }; 36 | }; 37 | 38 | public refresh = (): Promise> => { 39 | BarrelDetailsStoreProvider.clearCache(); 40 | ScreensStoreProvider.clearCache(); 41 | return this.get(); 42 | }; 43 | } 44 | 45 | export default new ActivityStore(); 46 | -------------------------------------------------------------------------------- /src/sidebar/activity/model/Activity.ts: -------------------------------------------------------------------------------- 1 | import User from "../../../common/domain/componentLike/model/User"; 2 | import Version from "../../../common/domain/componentLike/model/Version"; 3 | import { getDateAgo } from "../util/activityUi"; 4 | import ZeplinUriProvider from "../../../common/domain/openInZeplin/model/ZeplinUriProvider"; 5 | import ApplicationType from "../../../common/domain/openInZeplin/model/ApplicationType"; 6 | import ZeplinLinkType from "../../../common/domain/openInZeplin/model/ZeplinLinkType"; 7 | 8 | export default abstract class Activity implements ZeplinUriProvider { 9 | public date: Date; 10 | public dateAgo: string; 11 | public user?: User; 12 | 13 | public constructor(public itemName: string, version: Version) { 14 | this.date = new Date(version.created); 15 | this.dateAgo = getDateAgo(this.date); 16 | this.user = version.creator; 17 | } 18 | 19 | public abstract getZeplinUri(applicationType: ApplicationType): string; 20 | 21 | public abstract getZeplinLinkType(): ZeplinLinkType; 22 | } 23 | -------------------------------------------------------------------------------- /src/sidebar/activity/model/ComponentActivity.ts: -------------------------------------------------------------------------------- 1 | import ZeplinComponent from "../../../common/domain/zeplinComponent/model/ZeplinComponent"; 2 | import Activity from "./Activity"; 3 | import { getComponentUri } from "../../../common/domain/openInZeplin/util/zeplinUris"; 4 | import ApplicationType from "../../../common/domain/openInZeplin/model/ApplicationType"; 5 | import BarrelType from "../../../common/domain/barrel/BarrelType"; 6 | import ZeplinLinkType from "../../../common/domain/openInZeplin/model/ZeplinLinkType"; 7 | 8 | export default class ComponentActivity extends Activity { 9 | public constructor(private component: ZeplinComponent, private barrelId: string, private barrelType: BarrelType) { 10 | super(component.name, component.latestVersion); 11 | } 12 | 13 | public getZeplinUri(applicationType: ApplicationType): string { 14 | return getComponentUri(this.barrelId, this.barrelType, this.component._id, applicationType); 15 | } 16 | 17 | public getZeplinLinkType(): ZeplinLinkType { 18 | return ZeplinLinkType.Component; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/sidebar/activity/model/ScreenActivity.ts: -------------------------------------------------------------------------------- 1 | import Activity from "./Activity"; 2 | import ResponseScreen from "../../screen/model/ResponseScreen"; 3 | import { getScreenUri } from "../../../common/domain/openInZeplin/util/zeplinUris"; 4 | import ApplicationType from "../../../common/domain/openInZeplin/model/ApplicationType"; 5 | import ZeplinLinkType from "../../../common/domain/openInZeplin/model/ZeplinLinkType"; 6 | 7 | export default class ScreenActivity extends Activity { 8 | public constructor(private screen: ResponseScreen, private projectId: string) { 9 | super(screen.name, screen.latestVersion); 10 | } 11 | 12 | public getZeplinUri(applicationType: ApplicationType): string { 13 | return getScreenUri(this.projectId, this.screen._id, applicationType); 14 | } 15 | 16 | public getZeplinLinkType(): ZeplinLinkType { 17 | return ZeplinLinkType.Screen; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/sidebar/activity/tree/ActivityErrorsTreeItem.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import TreeItem from "../../../common/vscode/tree/TreeItem"; 3 | import localization from "../../../localization"; 4 | import BaseError from "../../../common/domain/error/BaseError"; 5 | import BarrelError from "../../../common/domain/zeplinComponent/model/BarrelError"; 6 | import ScreensError from "../../screen/model/ScreensError"; 7 | 8 | export default class ActivityErrorsTreeItem extends TreeItem { 9 | public constructor(private errors: BaseError[]) { 10 | super(localization.sidebar.activity.errors, undefined, undefined, vscode.TreeItemCollapsibleState.Collapsed); 11 | } 12 | 13 | public getChildren(): TreeItem[] { 14 | return this.errors.map(error => new TreeItem(this.getErrorMessage(error), this)); 15 | } 16 | 17 | private getErrorMessage(error: BaseError) { 18 | if (error instanceof BarrelError) { 19 | return localization.sidebar.activity.componentsError(error.id); 20 | } else if (error instanceof ScreensError) { 21 | return localization.sidebar.activity.screensError(error.barrelId); 22 | } else { 23 | return error.message; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/sidebar/activity/tree/ActivitySlotTreeItem.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import TreeItem from "../../../common/vscode/tree/TreeItem"; 3 | import Activity from "../model/Activity"; 4 | import ActivityTreeItem from "./ActivityTreeItem"; 5 | import { getThemedIconUris } from "../../../common/general/iconPathUtil"; 6 | 7 | export default class ActivitySlotTreeItem extends TreeItem { 8 | public iconPath = getThemedIconUris("icon-calendar.svg"); 9 | 10 | public constructor(slot: string, private activities: Activity[]) { 11 | super(slot, undefined, undefined, vscode.TreeItemCollapsibleState.Collapsed); 12 | } 13 | 14 | public getChildren(): TreeItem[] { 15 | return this.activities.map(activity => new ActivityTreeItem(activity, this)); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/sidebar/activity/tree/ActivityTreeDataProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import TreeDataProvider from "../../../common/vscode/tree/TreeDataProvider"; 3 | import TreeItem from "../../../common/vscode/tree/TreeItem"; 4 | import ActivityStore from "../data/ActivityStore"; 5 | import { getDateSlot } from "../util/activityUi"; 6 | import Activity from "../model/Activity"; 7 | import ActivitySlotTreeItem from "./ActivitySlotTreeItem"; 8 | import ActivityErrorsTreeItem from "./ActivityErrorsTreeItem"; 9 | import localization from "../../../localization"; 10 | import Refresher from "../../../session/util/Refresher"; 11 | 12 | class ActivityTreeDataProvider extends TreeDataProvider { 13 | protected viewId = "zeplin.views.activity"; 14 | protected showCollapseAll = true; 15 | 16 | public register(): vscode.Disposable { 17 | const disposables = [super.register()]; 18 | disposables.push( 19 | this.treeView!.onDidChangeVisibility(({ visible }) => { 20 | if (visible) { 21 | Refresher.requestRefresh(); 22 | } 23 | }) 24 | ); 25 | return vscode.Disposable.from(...disposables); 26 | } 27 | 28 | public async getRoots(): Promise { 29 | const { data, errors } = await ActivityStore.get(); 30 | 31 | const roots: TreeItem[] = errors?.length ? [new ActivityErrorsTreeItem(errors)] : []; 32 | 33 | const slots: { [slot: string]: Activity[] } = {}; 34 | const activities = data!.sort((first, second) => second.date.getTime() - first.date.getTime()); 35 | activities.forEach(activity => { 36 | const slot = getDateSlot(activity.date); 37 | if (!slots[slot]) { 38 | slots[slot] = []; 39 | } 40 | slots[slot].push(activity); 41 | }); 42 | 43 | roots.push(...Object.keys(slots).map(slot => new ActivitySlotTreeItem(slot, slots[slot]))); 44 | if (!roots.length) { 45 | roots.push(new TreeItem(localization.sidebar.activity.noneFound, undefined)); 46 | } 47 | return roots; 48 | } 49 | } 50 | 51 | export default new ActivityTreeDataProvider(); 52 | -------------------------------------------------------------------------------- /src/sidebar/activity/tree/ActivityTreeItem.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import TreeItem from "../../../common/vscode/tree/TreeItem"; 3 | import Activity from "../model/Activity"; 4 | import { getEmotarUrl } from "../../../common/domain/image/zeplinImageUtil"; 5 | import localization from "../../../localization"; 6 | import TreeItemContextProvider from "../../../common/vscode/tree/TreeItemContextProvider"; 7 | import TreeItemContext from "../../../common/domain/tree/TreeItemContext"; 8 | import ZeplinUriProvider from "../../../common/domain/openInZeplin/model/ZeplinUriProvider"; 9 | import ApplicationType from "../../../common/domain/openInZeplin/model/ApplicationType"; 10 | import ZeplinLinkType from "../../../common/domain/openInZeplin/model/ZeplinLinkType"; 11 | 12 | const contextProvider = new TreeItemContextProvider( 13 | TreeItemContext.Activity, 14 | TreeItemContext.ZeplinLink 15 | ); 16 | 17 | export default class ActivityTreeItem extends TreeItem implements ZeplinUriProvider { 18 | public iconPath = this.getThumbnail(); 19 | public description = this.activity.dateAgo; 20 | public tooltip = this.activity.date.toLocaleString(); 21 | 22 | public constructor(public activity: Activity, parent: TreeItem | undefined) { 23 | super(ActivityTreeItem.getLabel(activity), parent, contextProvider); 24 | } 25 | 26 | private static getLabel(activity: Activity): string { 27 | return !activity.user 28 | ? localization.sidebar.activity.updated(activity.itemName) 29 | : localization.sidebar.activity.updatedByUser(activity.user.username, activity.itemName); 30 | } 31 | 32 | private getThumbnail(): vscode.Uri | undefined { 33 | if (this.activity.user?.avatar) { 34 | return vscode.Uri.parse(this.activity.user.avatar); 35 | } else if (this.activity.user?.emotar) { 36 | return vscode.Uri.parse(getEmotarUrl(this.activity.user.emotar)); 37 | } else { 38 | return undefined; 39 | } 40 | } 41 | 42 | public getZeplinUri(applicationType: ApplicationType): string { 43 | return this.activity.getZeplinUri(applicationType); 44 | } 45 | 46 | public getZeplinLinkType(): ZeplinLinkType { 47 | return this.activity.getZeplinLinkType(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/sidebar/barrel/command/AddBarrelToSidebarCommand.ts: -------------------------------------------------------------------------------- 1 | import Command from "../../../common/vscode/command/Command"; 2 | import { startAddBarrelToSidebarFlowWithTypeSelection } from "../flow/barrelFlow"; 3 | 4 | class AddBarrelToSidebarCommand implements Command { 5 | public name = "zeplin.sidebar.addBarrel"; 6 | public execute = startAddBarrelToSidebarFlowWithTypeSelection; 7 | } 8 | 9 | export default new AddBarrelToSidebarCommand(); 10 | -------------------------------------------------------------------------------- /src/sidebar/barrel/command/AddProjectToSidebarCommand.ts: -------------------------------------------------------------------------------- 1 | import Command from "../../../common/vscode/command/Command"; 2 | import { startAddProjectToSidebarFlow } from "../flow/barrelFlow"; 3 | 4 | class AddProjectToSidebarCommand implements Command { 5 | public name = "zeplin.sidebar.addProject"; 6 | public execute = startAddProjectToSidebarFlow; 7 | } 8 | 9 | export default new AddProjectToSidebarCommand(); 10 | -------------------------------------------------------------------------------- /src/sidebar/barrel/command/AddStyleguideToSidebarCommand.ts: -------------------------------------------------------------------------------- 1 | import Command from "../../../common/vscode/command/Command"; 2 | import { startAddStyleguideToSidebarFlow } from "../flow/barrelFlow"; 3 | 4 | class AddStyleguideToSidebarCommand implements Command { 5 | public name = "zeplin.sidebar.addStyleguide"; 6 | public execute = startAddStyleguideToSidebarFlow; 7 | } 8 | 9 | export default new AddStyleguideToSidebarCommand(); 10 | -------------------------------------------------------------------------------- /src/sidebar/barrel/command/RemoveBarrelFromSidebarCommand.ts: -------------------------------------------------------------------------------- 1 | import Command from "../../../common/vscode/command/Command"; 2 | import { removeBarrel } from "../util/barrelUtil"; 3 | import { BarrelTreeItem } from "../tree/BarrelTreeItem"; 4 | 5 | class RemoveBarrelFromSidebarCommand implements Command { 6 | public name = "zeplin.sidebar.removeBarrel"; 7 | 8 | public execute(item: BarrelTreeItem) { 9 | removeBarrel(item.barrel); 10 | return Promise.resolve(); 11 | } 12 | } 13 | 14 | export default new RemoveBarrelFromSidebarCommand(); 15 | -------------------------------------------------------------------------------- /src/sidebar/barrel/tree/AddBarrelTreeItem.ts: -------------------------------------------------------------------------------- 1 | import TreeItem from "../../../common/vscode/tree/TreeItem"; 2 | import localization from "../../../localization"; 3 | import AddBarrelToSidebarCommand from "../command/AddBarrelToSidebarCommand"; 4 | import { getThemedIconUris } from "../../../common/general/iconPathUtil"; 5 | 6 | export default class AddBarrelTreeItem extends TreeItem { 7 | public iconPath = getThemedIconUris("icon-add.svg"); 8 | public command = { 9 | title: localization.sidebar.barrel.addAnother, 10 | command: AddBarrelToSidebarCommand.name 11 | }; 12 | 13 | public constructor() { 14 | super(localization.sidebar.barrel.addAnother, undefined); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/sidebar/barrel/tree/BarrelTreeItem.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import TreeItem from "../../../common/vscode/tree/TreeItem"; 3 | import Barrel from "../../../common/domain/barrel/Barrel"; 4 | import ScreensTreeItem from "../../screen/tree/ScreensTreeItem"; 5 | import ZeplinComponentsTreeItem from "../../zeplinComponent/tree/ZeplinComponentsTreeItem"; 6 | import TreeItemContext from "../../../common/domain/tree/TreeItemContext"; 7 | import TreeItemContextProvider from "../../../common/vscode/tree/TreeItemContextProvider"; 8 | import BarrelType from "../../../common/domain/barrel/BarrelType"; 9 | import ZeplinUriProvider from "../../../common/domain/openInZeplin/model/ZeplinUriProvider"; 10 | import ApplicationType from "../../../common/domain/openInZeplin/model/ApplicationType"; 11 | import { getBarrelUri } from "../../../common/domain/openInZeplin/util/zeplinUris"; 12 | import ZeplinLinkType from "../../../common/domain/openInZeplin/model/ZeplinLinkType"; 13 | 14 | function getContextProvider(barrel: Barrel): TreeItemContextProvider { 15 | const contexts = [TreeItemContext.Barrel, TreeItemContext.ZeplinLink]; 16 | if (barrel.jiras.length) { 17 | contexts.push(TreeItemContext.Jira); 18 | } 19 | return new TreeItemContextProvider(...contexts); 20 | } 21 | 22 | export class BarrelTreeItem extends TreeItem implements ZeplinUriProvider { 23 | public constructor(public barrel: Barrel) { 24 | super(barrel.name, undefined, getContextProvider(barrel), vscode.TreeItemCollapsibleState.Collapsed); 25 | this.setRemoteIconPath(barrel.thumbnail); 26 | } 27 | 28 | public getChildren(): TreeItem[] { 29 | const children: TreeItem[] = [new ZeplinComponentsTreeItem(this.barrel, this)]; 30 | if (this.barrel.type === BarrelType.Project) { 31 | children.unshift(new ScreensTreeItem(this.barrel, this)); 32 | } 33 | 34 | return children; 35 | } 36 | 37 | public getZeplinUri(applicationType: ApplicationType): string { 38 | return getBarrelUri(this.barrel.id, this.barrel.type, applicationType); 39 | } 40 | 41 | public getZeplinLinkType(): ZeplinLinkType { 42 | return this.barrel.type === BarrelType.Project ? ZeplinLinkType.Project : ZeplinLinkType.Styleguide; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/sidebar/barrel/tree/NoBarrelTreeItem.ts: -------------------------------------------------------------------------------- 1 | import TreeItem from "../../../common/vscode/tree/TreeItem"; 2 | import localization from "../../../localization"; 3 | import { getThemedIconUris } from "../../../common/general/iconPathUtil"; 4 | 5 | export default class NoBarrelTreeItem extends TreeItem { 6 | public iconPath = getThemedIconUris("icon-info.svg"); 7 | 8 | public constructor() { 9 | super(localization.sidebar.barrel.emptyInfo, undefined); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/sidebar/barrel/util/barrelUtil.ts: -------------------------------------------------------------------------------- 1 | import Barrel from "../../../common/domain/barrel/Barrel"; 2 | import ContextProvider from "../../../common/vscode/extension/ContextProvider"; 3 | import BarrelTreeDataProvider from "../tree/BarrelTreeDataProvider"; 4 | import ActivityTreeDataProvider from "../../activity/tree/ActivityTreeDataProvider"; 5 | import { removeBarrelItemsFromPinnedItems } from "../../pin/util/pinUtil"; 6 | import ScreensStoreProvider from "../../screen/data/ScreensStoreProvider"; 7 | import BarrelDetailsStoreProvider from "../../../common/domain/zeplinComponent/data/BarrelDetailsStoreProvider"; 8 | 9 | const KEY_SAVED_BARRELS = "sidebar.savedBarrels"; 10 | 11 | function getSavedBarrels(): Barrel[] { 12 | return ContextProvider.get().workspaceState.get(KEY_SAVED_BARRELS) ?? []; 13 | } 14 | 15 | function saveBarrels(barrels: Barrel[]) { 16 | ContextProvider.get().workspaceState.update(KEY_SAVED_BARRELS, barrels); 17 | 18 | BarrelTreeDataProvider.refresh(); 19 | ActivityTreeDataProvider.refresh(); 20 | } 21 | 22 | function isBarrelSaved(barrel: Barrel): boolean { 23 | return getSavedBarrels().some(savedBarrel => savedBarrel.id === barrel.id); 24 | } 25 | 26 | function saveBarrel(barrel: Barrel) { 27 | const barrels = getSavedBarrels(); 28 | const updateIndex = barrels.findIndex(savedBarrel => savedBarrel.id === barrel.id); 29 | if (updateIndex >= 0) { 30 | barrels[updateIndex] = barrel; 31 | } else { 32 | barrels.push(barrel); 33 | } 34 | saveBarrels(barrels); 35 | } 36 | 37 | function removeBarrel(barrel: Barrel) { 38 | const barrels = getSavedBarrels().filter(savedBarrel => savedBarrel.id !== barrel.id); 39 | saveBarrels(barrels); 40 | ScreensStoreProvider.clearCacheFor(barrel.id); 41 | BarrelDetailsStoreProvider.clearCacheFor(barrel.id); 42 | removeBarrelItemsFromPinnedItems(barrel); 43 | } 44 | 45 | export { 46 | getSavedBarrels, 47 | isBarrelSaved, 48 | saveBarrel, 49 | removeBarrel 50 | }; 51 | -------------------------------------------------------------------------------- /src/sidebar/jira/command/OpenJiraLinkCommand.ts: -------------------------------------------------------------------------------- 1 | import Command from "../../../common/vscode/command/Command"; 2 | import { startOpenJiraLinkFlow } from "../flow/jiraFlow"; 3 | 4 | class OpenJiraLinkCommand implements Command { 5 | public name = "zeplin.sidebar.openJiraLink"; 6 | public execute = startOpenJiraLinkFlow; 7 | } 8 | 9 | export default new OpenJiraLinkCommand(); 10 | -------------------------------------------------------------------------------- /src/sidebar/jumpTo/command/JumpToSidebarItemCommand.ts: -------------------------------------------------------------------------------- 1 | import Command from "../../../common/vscode/command/Command"; 2 | import { startJumpToFlow } from "../flow/jumpToFlow"; 3 | 4 | class JumpToSidebarItemCommand implements Command { 5 | public name = "zeplin.sidebar.jumpTo"; 6 | public execute = startJumpToFlow; 7 | } 8 | 9 | export default new JumpToSidebarItemCommand(); 10 | -------------------------------------------------------------------------------- /src/sidebar/jumpTo/tree/JumpToTreeItem.ts: -------------------------------------------------------------------------------- 1 | import TreeItem from "../../../common/vscode/tree/TreeItem"; 2 | import localization from "../../../localization"; 3 | import JumpToSidebarItemCommand from "../command/JumpToSidebarItemCommand"; 4 | import { getThemedIconUris } from "../../../common/general/iconPathUtil"; 5 | 6 | export default class JumpToTreeItem extends TreeItem { 7 | public iconPath = getThemedIconUris("icon-search.svg"); 8 | public command = { 9 | title: localization.sidebar.jumpTo.jumpToItem, 10 | command: JumpToSidebarItemCommand.name 11 | }; 12 | 13 | public constructor() { 14 | super(localization.sidebar.jumpTo.jumpToItem, undefined); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/sidebar/openInZeplin/command/OpenInZeplinOnDoubleClickCommand.ts: -------------------------------------------------------------------------------- 1 | import Command from "../../../common/vscode/command/Command"; 2 | import { openInZeplin } from "../../../common/domain/openInZeplin/flow/openInZeplinFlow"; 3 | import ZeplinUriProvider from "../../../common/domain/openInZeplin/model/ZeplinUriProvider"; 4 | 5 | const DOUBLE_CLICK_DURATION = 500; 6 | 7 | class OpenInZeplinOnDoubleClickCommand implements Command { 8 | public name = "zeplin.sidebar.openInZeplinOnDoubleClick"; 9 | private lastClickTime?: number; 10 | private lastClickedItem?: ZeplinUriProvider; 11 | 12 | public execute(uriProvider: ZeplinUriProvider) { 13 | if ( 14 | this.lastClickTime && this.lastClickedItem && this.lastClickedItem === uriProvider && 15 | this.lastClickTime > Date.now() - DOUBLE_CLICK_DURATION 16 | ) { 17 | this.lastClickTime = undefined; 18 | this.lastClickedItem = undefined; 19 | return openInZeplin(uriProvider); 20 | } else { 21 | this.lastClickTime = Date.now(); 22 | this.lastClickedItem = uriProvider; 23 | return Promise.resolve(); 24 | } 25 | } 26 | } 27 | 28 | export default new OpenInZeplinOnDoubleClickCommand(); 29 | -------------------------------------------------------------------------------- /src/sidebar/pin/command/PinComponentToSidebarCommand.ts: -------------------------------------------------------------------------------- 1 | import Command from "../../../common/vscode/command/Command"; 2 | import { startPinComponentFlow } from "../flow/pinFlow"; 3 | 4 | class PinComponentToSidebarCommand implements Command { 5 | public name = "zeplin.sidebar.pinComponent"; 6 | public execute = startPinComponentFlow; 7 | } 8 | 9 | export default new PinComponentToSidebarCommand(); 10 | -------------------------------------------------------------------------------- /src/sidebar/pin/command/PinScreenToSidebarCommand.ts: -------------------------------------------------------------------------------- 1 | import Command from "../../../common/vscode/command/Command"; 2 | import { startPinScreenFlow } from "../flow/pinFlow"; 3 | 4 | class PinScreenToSidebarCommand implements Command { 5 | public name = "zeplin.sidebar.pinScreen"; 6 | public execute = startPinScreenFlow; 7 | } 8 | 9 | export default new PinScreenToSidebarCommand(); 10 | -------------------------------------------------------------------------------- /src/sidebar/pin/command/PinToSidebarCommand.ts: -------------------------------------------------------------------------------- 1 | import Command from "../../../common/vscode/command/Command"; 2 | import { pinItem } from "../flow/pinFlow"; 3 | import TreeItem from "../../../common/vscode/tree/TreeItem"; 4 | 5 | class PinToSidebarCommand implements Command { 6 | public name = "zeplin.sidebar.pinItem"; 7 | 8 | public execute(item: TreeItem) { 9 | pinItem(item); 10 | return Promise.resolve(); 11 | } 12 | } 13 | 14 | export default new PinToSidebarCommand(); 15 | -------------------------------------------------------------------------------- /src/sidebar/pin/command/UnpinAllFromSidebarCommand.ts: -------------------------------------------------------------------------------- 1 | import Command from "../../../common/vscode/command/Command"; 2 | import { askUnpinAll } from "../flow/pinFlow"; 3 | 4 | class UnpinAllFromSidebarCommand implements Command { 5 | public name = "zeplin.sidebar.unpinAll"; 6 | 7 | public execute() { 8 | askUnpinAll(); 9 | return Promise.resolve(); 10 | } 11 | } 12 | 13 | export default new UnpinAllFromSidebarCommand(); 14 | -------------------------------------------------------------------------------- /src/sidebar/pin/command/UnpinFromSidebarCommand.ts: -------------------------------------------------------------------------------- 1 | import Command from "../../../common/vscode/command/Command"; 2 | import { unpinItem } from "../flow/pinFlow"; 3 | import TreeItem from "../../../common/vscode/tree/TreeItem"; 4 | 5 | class UnpinFromSidebarCommand implements Command { 6 | public name = "zeplin.sidebar.unpinItem"; 7 | 8 | public execute(item: TreeItem) { 9 | unpinItem(item); 10 | return Promise.resolve(); 11 | } 12 | } 13 | 14 | export default new UnpinFromSidebarCommand(); 15 | -------------------------------------------------------------------------------- /src/sidebar/pin/data/PinnableComponentsStore.ts: -------------------------------------------------------------------------------- 1 | import Store from "../../../common/domain/store/Store"; 2 | import Result from "../../../common/domain/store/Result"; 3 | import BarrelDetailsStoreProvider from "../../../common/domain/zeplinComponent/data/BarrelDetailsStoreProvider"; 4 | import { getPinnedItems } from "../util/pinUtil"; 5 | import PinType from "../model/PinType"; 6 | import ComponentPinData from "../model/ComponentPinData"; 7 | import BarrelType from "../../../common/domain/barrel/BarrelType"; 8 | import ZeplinComponentsStore from "../../../common/domain/zeplinComponent/data/ZeplinComponentsStore"; 9 | import ZeplinComponent from "../../../common/domain/zeplinComponent/model/ZeplinComponent"; 10 | 11 | export default class PinnableComponentsStore implements Store { 12 | public constructor(private barrelId: string, private barrelType: BarrelType) { } 13 | 14 | public get = async (): Promise> => { 15 | const { data: components, errors } = await new ZeplinComponentsStore(this.barrelId, this.barrelType).get(); 16 | 17 | if (errors?.length) { 18 | return { 19 | errors 20 | }; 21 | } else { 22 | const pinnedComponentIds = getPinnedItems() 23 | .filter(item => item.type === PinType.Component) 24 | .map(item => (item as ComponentPinData).component._id); 25 | return { 26 | data: components!.filter(component => !pinnedComponentIds.includes(component._id)) 27 | }; 28 | } 29 | }; 30 | 31 | public refresh = (): Promise> => { 32 | BarrelDetailsStoreProvider.clearCache(); 33 | return this.get(); 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /src/sidebar/pin/data/PinnableScreensStore.ts: -------------------------------------------------------------------------------- 1 | import Store from "../../../common/domain/store/Store"; 2 | import Result from "../../../common/domain/store/Result"; 3 | import BarrelDetailsStoreProvider from "../../../common/domain/zeplinComponent/data/BarrelDetailsStoreProvider"; 4 | import ScreensStoreProvider from "../../screen/data/ScreensStoreProvider"; 5 | import Screen from "../../screen/model/Screen"; 6 | import ProjectScreensStore from "../../screen/data/ProjectScreensStore"; 7 | import { flatten } from "../../../common/general/arrayUtil"; 8 | import { getPinnedItems } from "../util/pinUtil"; 9 | import PinType from "../model/PinType"; 10 | import ScreenPinData from "../model/ScreenPinData"; 11 | 12 | export default class PinnableScreensStore implements Store { 13 | public constructor(private projectId: string) { } 14 | 15 | public get = async (): Promise> => { 16 | const { data: projectScreens, errors } = await new ProjectScreensStore(this.projectId).get(); 17 | 18 | if (errors?.length) { 19 | return { 20 | errors 21 | }; 22 | } else { 23 | const { screens, sections } = projectScreens!; 24 | const pinnedScreenIds = getPinnedItems() 25 | .filter(item => item.type === PinType.Screen) 26 | .map(item => (item as ScreenPinData).screen._id); 27 | return { 28 | data: flatten([screens, ...sections.map(section => section.screens)]) 29 | .filter(screen => !pinnedScreenIds.includes(screen._id)) 30 | }; 31 | } 32 | }; 33 | 34 | public refresh = (): Promise> => { 35 | BarrelDetailsStoreProvider.clearCache(); 36 | ScreensStoreProvider.clearCache(); 37 | return this.get(); 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/sidebar/pin/model/ComponentPinData.ts: -------------------------------------------------------------------------------- 1 | import PinData from "./PinData"; 2 | import ResponseZeplinComponent from "../../../common/domain/zeplinComponent/model/ResponseZeplinComponent"; 3 | 4 | export default interface ComponentPinData extends PinData { 5 | component: ResponseZeplinComponent; 6 | } 7 | -------------------------------------------------------------------------------- /src/sidebar/pin/model/PinData.ts: -------------------------------------------------------------------------------- 1 | import PinType from "./PinType"; 2 | import Barrel from "../../../common/domain/barrel/Barrel"; 3 | 4 | export default interface PinData { 5 | type: PinType; 6 | barrel: Barrel; 7 | } 8 | -------------------------------------------------------------------------------- /src/sidebar/pin/model/PinType.ts: -------------------------------------------------------------------------------- 1 | enum PinType { 2 | Screen, 3 | Component 4 | } 5 | 6 | export default PinType; 7 | -------------------------------------------------------------------------------- /src/sidebar/pin/model/ScreenPinData.ts: -------------------------------------------------------------------------------- 1 | import PinData from "./PinData"; 2 | import Screen from "../../screen/model/Screen"; 3 | 4 | export default interface ScreenPinData extends PinData { 5 | screen: Screen; 6 | } 7 | -------------------------------------------------------------------------------- /src/sidebar/pin/tree/NoPinnedItemTreeItem.ts: -------------------------------------------------------------------------------- 1 | import TreeItem from "../../../common/vscode/tree/TreeItem"; 2 | import localization from "../../../localization"; 3 | import { getThemedIconUris } from "../../../common/general/iconPathUtil"; 4 | 5 | export default class NoPinnedItemTreeItem extends TreeItem { 6 | public iconPath = getThemedIconUris("icon-pinned.svg"); 7 | 8 | public constructor() { 9 | super(localization.sidebar.pin.emptyInfo, undefined); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/sidebar/refresh/command/RefreshSidebarCommand.ts: -------------------------------------------------------------------------------- 1 | import Command from "../../../common/vscode/command/Command"; 2 | import Refresher from "../../../session/util/Refresher"; 3 | 4 | class RefreshSidebarCommand implements Command { 5 | public name = "zeplin.sidebar.refresh"; 6 | 7 | public execute() { 8 | Refresher.refresh(); 9 | return Promise.resolve(); 10 | } 11 | } 12 | 13 | export default new RefreshSidebarCommand(); 14 | -------------------------------------------------------------------------------- /src/sidebar/refresh/util/refreshUtil.ts: -------------------------------------------------------------------------------- 1 | import BarrelTreeDataProvider from "../../barrel/tree/BarrelTreeDataProvider"; 2 | import ActivityTreeDataProvider from "../../activity/tree/ActivityTreeDataProvider"; 3 | import BarrelDetails from "../../../common/domain/zeplinComponent/model/BarrelDetails"; 4 | import { updatePinnedScreens, updatePinnedItems } from "../../pin/util/pinUtil"; 5 | import ResponseScreen from "../../screen/model/ResponseScreen"; 6 | import TreeItem from "../../../common/vscode/tree/TreeItem"; 7 | import PinTreeDataProvider from "../../pin/tree/PinTreeDataProvider"; 8 | 9 | function updateSidebarScreens(projectId: string, screens: ResponseScreen[]) { 10 | updatePinnedScreens(projectId, screens); 11 | BarrelTreeDataProvider.refresh(); 12 | ActivityTreeDataProvider.refresh(); 13 | } 14 | 15 | function updateSidebarItems(barrel: BarrelDetails) { 16 | updatePinnedItems(barrel); 17 | BarrelTreeDataProvider.refresh(); 18 | ActivityTreeDataProvider.refresh(); 19 | } 20 | 21 | function refreshItem(item: TreeItem) { 22 | BarrelTreeDataProvider.refreshItem(item); 23 | PinTreeDataProvider.refreshItem(item); 24 | ActivityTreeDataProvider.refreshItem(item); 25 | } 26 | 27 | export { 28 | updateSidebarScreens, 29 | updateSidebarItems, 30 | refreshItem 31 | }; 32 | -------------------------------------------------------------------------------- /src/sidebar/screen/data/ScreensStore.ts: -------------------------------------------------------------------------------- 1 | import BasicStore from "../../../common/domain/store/BasicStore"; 2 | import { getScreens } from "../../../common/domain/api/api"; 3 | import ResponseScreen from "../model/ResponseScreen"; 4 | import ScreensResponse from "../model/ScreensResponse"; 5 | import BaseError from "../../../common/domain/error/BaseError"; 6 | import ScreensError from "../model/ScreensError"; 7 | 8 | export default class ScreensStore extends BasicStore { 9 | public constructor(private projectId: string) { 10 | super(); 11 | } 12 | 13 | protected async fetchData(): Promise { 14 | const result = await getScreens(this.projectId); 15 | return result instanceof BaseError 16 | ? new ScreensError(this.projectId, result.message, result.code) 17 | : result; 18 | } 19 | 20 | protected extractData(response: ScreensResponse): ResponseScreen[] { 21 | return response.screens; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/sidebar/screen/data/ScreensStoreProvider.ts: -------------------------------------------------------------------------------- 1 | import CacheHolder from "../../../common/domain/store/CacheHolder"; 2 | import ScreensStore from "./ScreensStore"; 3 | import { updateSidebarScreens } from "../../refresh/util/refreshUtil"; 4 | 5 | class ScreensStoreProvider implements CacheHolder { 6 | private cache: { [id: string]: ScreensStore } = {}; 7 | 8 | public get(id: string): ScreensStore { 9 | if (!this.cache[id]) { 10 | this.cache[id] = new ScreensStore(id); 11 | this.cache[id].onDataReceived(screens => updateSidebarScreens(id, screens)); 12 | } 13 | 14 | return this.cache[id]; 15 | } 16 | 17 | public clearCache() { 18 | Object.keys(this.cache).forEach(key => this.cache[key].dispose()); 19 | this.cache = {}; 20 | } 21 | 22 | public clearCacheFor(id: string) { 23 | this.cache[id]?.dispose(); 24 | delete this.cache[id]; 25 | } 26 | } 27 | 28 | export default new ScreensStoreProvider(); 29 | -------------------------------------------------------------------------------- /src/sidebar/screen/model/ProjectScreens.ts: -------------------------------------------------------------------------------- 1 | import Screen from "./Screen"; 2 | import ScreenSection from "./ScreenSection"; 3 | 4 | export default interface ProjectScreens { 5 | screens: Screen[]; 6 | sections: ScreenSection[]; 7 | } 8 | -------------------------------------------------------------------------------- /src/sidebar/screen/model/ResponseScreen.ts: -------------------------------------------------------------------------------- 1 | import ComponentLike from "../../../common/domain/componentLike/model/ComponentLike"; 2 | 3 | export default interface ResponseScreen extends ComponentLike { 4 | } 5 | -------------------------------------------------------------------------------- /src/sidebar/screen/model/Screen.ts: -------------------------------------------------------------------------------- 1 | import JiraAttachable from "../../../common/domain/jira/model/JiraAttachable"; 2 | import ResponseScreen from "./ResponseScreen"; 3 | 4 | export default interface Screen extends ResponseScreen, JiraAttachable { 5 | barrelId: string; 6 | barrelName: string; 7 | sectionId?: string; 8 | sectionName?: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/sidebar/screen/model/ScreenSection.ts: -------------------------------------------------------------------------------- 1 | import Screen from "./Screen"; 2 | import JiraAttachable from "../../../common/domain/jira/model/JiraAttachable"; 3 | 4 | export default interface ScreenSection extends JiraAttachable { 5 | id: string; 6 | name: string; 7 | description?: string; 8 | screens: Screen[]; 9 | } 10 | -------------------------------------------------------------------------------- /src/sidebar/screen/model/ScreensError.ts: -------------------------------------------------------------------------------- 1 | import BaseError from "../../../common/domain/error/BaseError"; 2 | 3 | export default class ScreensError extends BaseError { 4 | public constructor(public barrelId: string, message?: string, code?: number) { 5 | super(message, code); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/sidebar/screen/model/ScreensResponse.ts: -------------------------------------------------------------------------------- 1 | import ResponseScreen from "./ResponseScreen"; 2 | 3 | export default interface ScreensResponse { 4 | screens: ResponseScreen[]; 5 | } 6 | -------------------------------------------------------------------------------- /src/sidebar/screen/tree/ScreenSectionTreeItem.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import TreeItem from "../../../common/vscode/tree/TreeItem"; 3 | import ScreenSection from "../model/ScreenSection"; 4 | import ScreenTreeItem from "./ScreenTreeItem"; 5 | import TreeItemContextProvider from "../../../common/vscode/tree/TreeItemContextProvider"; 6 | import TreeItemContext from "../../../common/domain/tree/TreeItemContext"; 7 | import Barrel from "../../../common/domain/barrel/Barrel"; 8 | import ZeplinUriProvider from "../../../common/domain/openInZeplin/model/ZeplinUriProvider"; 9 | import ApplicationType from "../../../common/domain/openInZeplin/model/ApplicationType"; 10 | import { getScreenSectionUri } from "../../../common/domain/openInZeplin/util/zeplinUris"; 11 | import ZeplinLinkType from "../../../common/domain/openInZeplin/model/ZeplinLinkType"; 12 | 13 | function getContextProvider(section: ScreenSection): TreeItemContextProvider { 14 | const contexts = [TreeItemContext.ScreenSection, TreeItemContext.ZeplinLink]; 15 | if (section.jiras.length) { 16 | contexts.push(TreeItemContext.Jira); 17 | } 18 | return new TreeItemContextProvider(...contexts); 19 | } 20 | 21 | export default class ScreenSectionTreeItem extends TreeItem implements ZeplinUriProvider { 22 | public constructor(public section: ScreenSection, public project: Barrel, parent: TreeItem | undefined) { 23 | super(section.name, parent, getContextProvider(section), vscode.TreeItemCollapsibleState.Collapsed); 24 | } 25 | 26 | public getChildren(): TreeItem[] { 27 | return this.section.screens.map(screen => new ScreenTreeItem(screen, this.project, this)); 28 | } 29 | 30 | public getZeplinUri(applicationType: ApplicationType): string { 31 | return getScreenSectionUri(this.project.id, this.section.id, applicationType); 32 | } 33 | 34 | public getZeplinLinkType(): ZeplinLinkType { 35 | return ZeplinLinkType.ScreenSection; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/sidebar/screen/tree/ScreenTreeItem.ts: -------------------------------------------------------------------------------- 1 | import Screen from "../model/Screen"; 2 | import TreeItem from "../../../common/vscode/tree/TreeItem"; 3 | import TreeItemContextProvider from "../../../common/vscode/tree/TreeItemContextProvider"; 4 | import TreeItemContext from "../../../common/domain/tree/TreeItemContext"; 5 | import Barrel from "../../../common/domain/barrel/Barrel"; 6 | import { isScreenPinned } from "../../pin/util/pinUtil"; 7 | import ZeplinUriProvider from "../../../common/domain/openInZeplin/model/ZeplinUriProvider"; 8 | import ApplicationType from "../../../common/domain/openInZeplin/model/ApplicationType"; 9 | import { getScreenUri } from "../../../common/domain/openInZeplin/util/zeplinUris"; 10 | import ZeplinLinkType from "../../../common/domain/openInZeplin/model/ZeplinLinkType"; 11 | 12 | function getContextProvider(screen: Screen): TreeItemContextProvider { 13 | const contexts = [ 14 | TreeItemContext.Screen, 15 | TreeItemContext.ZeplinLink, 16 | isScreenPinned(screen) ? TreeItemContext.Pinned : TreeItemContext.Pinnable 17 | ]; 18 | if (screen.jiras.length) { 19 | contexts.push(TreeItemContext.Jira); 20 | } 21 | return new TreeItemContextProvider(...contexts); 22 | } 23 | 24 | export default class ScreenTreeItem extends TreeItem implements ZeplinUriProvider { 25 | public constructor(public screen: Screen, public project: Barrel, parent: TreeItem | undefined) { 26 | super(screen.name, parent, getContextProvider(screen)); 27 | this.setRemoteIconPath(screen.latestVersion.snapshot.url); 28 | } 29 | 30 | public getZeplinUri(applicationType: ApplicationType): string { 31 | return getScreenUri(this.project.id, this.screen._id, applicationType); 32 | } 33 | 34 | public getZeplinLinkType(): ZeplinLinkType { 35 | return ZeplinLinkType.Screen; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/sidebar/screen/tree/ScreensTreeItem.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import Barrel from "../../../common/domain/barrel/Barrel"; 3 | import TreeItem from "../../../common/vscode/tree/TreeItem"; 4 | import localization from "../../../localization"; 5 | import ProjectScreensStore from "../data/ProjectScreensStore"; 6 | import ScreenSection from "../model/ScreenSection"; 7 | import ScreenTreeItem from "./ScreenTreeItem"; 8 | import ScreenSectionTreeItem from "./ScreenSectionTreeItem"; 9 | 10 | export default class ScreensTreeItem extends TreeItem { 11 | public constructor(private project: Barrel, parent: TreeItem | undefined) { 12 | super(localization.sidebar.screen.screens, parent, undefined, vscode.TreeItemCollapsibleState.Collapsed); 13 | } 14 | 15 | public async getChildren(): Promise { 16 | const { data, errors } = await new ProjectScreensStore(this.project.id).get(); 17 | if (!data) { 18 | const error = errors![0]; 19 | return [new TreeItem(error.message, this)]; 20 | } else { 21 | const { screens, sections } = data; 22 | const filledSections = sections?.filter(this.isSectionFilled) ?? []; 23 | if (screens.length || filledSections.length) { 24 | return [ 25 | ...screens.map(screen => new ScreenTreeItem(screen, this.project, this)), 26 | ...filledSections.map(section => new ScreenSectionTreeItem(section, this.project, this)) 27 | ]; 28 | } else { 29 | return [new TreeItem(localization.sidebar.screen.noneFound, this)]; 30 | } 31 | } 32 | } 33 | 34 | private isSectionFilled(section: ScreenSection) { 35 | return !!section.screens.length; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/sidebar/screen/util/screenUi.ts: -------------------------------------------------------------------------------- 1 | import Screen from "../model/Screen"; 2 | import localization from "../../../localization"; 3 | 4 | const DETAIL_ITEM_SEPARATOR = " ▸ "; 5 | 6 | function getScreenDetailRepresentation(screen: Screen): string { 7 | return [screen.barrelName, localization.sidebar.screen.screens, screen.sectionName] 8 | .filter(representation => representation) 9 | .join(DETAIL_ITEM_SEPARATOR); 10 | } 11 | 12 | export { 13 | getScreenDetailRepresentation 14 | }; 15 | -------------------------------------------------------------------------------- /src/sidebar/zeplinComponent/data/CumulativeBarrelDetailsStore.ts: -------------------------------------------------------------------------------- 1 | import BarrelDetails from "../../../common/domain/zeplinComponent/model/BarrelDetails"; 2 | import Store from "../../../common/domain/store/Store"; 3 | import BarrelError from "../../../common/domain/zeplinComponent/model/BarrelError"; 4 | import BarrelType from "../../../common/domain/barrel/BarrelType"; 5 | import Result from "../../../common/domain/store/Result"; 6 | import BarrelDetailsStoreProvider from "../../../common/domain/zeplinComponent/data/BarrelDetailsStoreProvider"; 7 | 8 | export default class CumulativeBarrelDetailsStore implements Store { 9 | public constructor(private barrelId: string, private barrelType: BarrelType) { } 10 | 11 | public get = async (): Promise> => { 12 | const leafId = this.barrelId; 13 | const leafType = this.barrelType; 14 | let currentId: string | undefined = leafId; 15 | let currentType = leafType; 16 | const accumulatedResult: Result = { 17 | data: [], 18 | errors: [] 19 | }; 20 | 21 | while (currentId) { 22 | const childId: string | undefined = currentId === leafId ? undefined : leafId; 23 | const childType: BarrelType | undefined = currentId === leafId ? undefined : leafType; 24 | 25 | const { data, errors }: Result = 26 | await BarrelDetailsStoreProvider.get(currentId, currentType, childId, childType).get(); 27 | 28 | if (errors) { 29 | accumulatedResult.errors!.push(...errors); 30 | currentId = undefined; 31 | } 32 | 33 | if (data) { 34 | accumulatedResult.data!.push(data); 35 | currentId = data.parentId; 36 | currentType = BarrelType.Styleguide; 37 | } 38 | } 39 | 40 | return accumulatedResult; 41 | }; 42 | 43 | public refresh = (): Promise> => { 44 | BarrelDetailsStoreProvider.clearCache(); 45 | return this.get(); 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /src/sidebar/zeplinComponent/tree/BarrelZeplinComponentsTreeItem.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import TreeItem from "../../../common/vscode/tree/TreeItem"; 3 | import BarrelDetails from "../../../common/domain/zeplinComponent/model/BarrelDetails"; 4 | import { createList } from "./zeplinComponentTreeUtil"; 5 | import TreeItemContextProvider from "../../../common/vscode/tree/TreeItemContextProvider"; 6 | import TreeItemContext from "../../../common/domain/tree/TreeItemContext"; 7 | import ZeplinUriProvider from "../../../common/domain/openInZeplin/model/ZeplinUriProvider"; 8 | import ApplicationType from "../../../common/domain/openInZeplin/model/ApplicationType"; 9 | import { getComponentsUri } from "../../../common/domain/openInZeplin/util/zeplinUris"; 10 | import BarrelType from "../../../common/domain/barrel/BarrelType"; 11 | import localization from "../../../localization"; 12 | import ZeplinLinkType from "../../../common/domain/openInZeplin/model/ZeplinLinkType"; 13 | 14 | const contextProvider = new TreeItemContextProvider( 15 | TreeItemContext.ZeplinComponentBarrel, 16 | TreeItemContext.ZeplinLink 17 | ); 18 | 19 | export default class BarrelZeplinComponentsTreeItem extends TreeItem implements ZeplinUriProvider { 20 | public constructor(public barrel: BarrelDetails, parent: TreeItem | undefined) { 21 | super( 22 | barrel.type === BarrelType.Project ? localization.sidebar.zeplinComponent.localStyleguide : barrel.name, 23 | parent, 24 | contextProvider, 25 | vscode.TreeItemCollapsibleState.Collapsed 26 | ); 27 | this.setRemoteIconPath(barrel.thumbnail); 28 | } 29 | 30 | public getChildren(): TreeItem[] { 31 | const [{ components }, ...sections] = this.barrel.componentSections; 32 | return createList(components, sections, this.barrel, this); 33 | } 34 | 35 | public getZeplinUri(applicationType: ApplicationType): string { 36 | return getComponentsUri(this.barrel.id, this.barrel.type, applicationType); 37 | } 38 | 39 | public getZeplinLinkType(): ZeplinLinkType { 40 | return this.barrel.type === BarrelType.Project ? ZeplinLinkType.Project : ZeplinLinkType.Styleguide; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/sidebar/zeplinComponent/tree/ZeplinComponentSectionTreeItem.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import TreeItem from "../../../common/vscode/tree/TreeItem"; 3 | import ZeplinComponentSection from "../../../common/domain/zeplinComponent/model/ZeplinComponentSection"; 4 | import { createList } from "./zeplinComponentTreeUtil"; 5 | import Barrel from "../../../common/domain/barrel/Barrel"; 6 | import TreeItemContextProvider from "../../../common/vscode/tree/TreeItemContextProvider"; 7 | import TreeItemContext from "../../../common/domain/tree/TreeItemContext"; 8 | import ZeplinUriProvider from "../../../common/domain/openInZeplin/model/ZeplinUriProvider"; 9 | import ApplicationType from "../../../common/domain/openInZeplin/model/ApplicationType"; 10 | import { getComponentSectionUri } from "../../../common/domain/openInZeplin/util/zeplinUris"; 11 | import ZeplinLinkType from "../../../common/domain/openInZeplin/model/ZeplinLinkType"; 12 | 13 | const contextProvider = new TreeItemContextProvider( 14 | TreeItemContext.ZeplinComponentSection, 15 | TreeItemContext.ZeplinLink 16 | ); 17 | 18 | export default class ZeplinComponentSectionTreeItem extends TreeItem implements ZeplinUriProvider { 19 | public constructor(public section: ZeplinComponentSection, public barrel: Barrel, parent: TreeItem | undefined) { 20 | super(section.name, parent, contextProvider, vscode.TreeItemCollapsibleState.Collapsed); 21 | } 22 | 23 | public getChildren(): TreeItem[] { 24 | return createList(this.section.components, this.section.componentSections, this.barrel, this); 25 | } 26 | 27 | public getZeplinUri(applicationType: ApplicationType): string { 28 | return getComponentSectionUri(this.barrel.id, this.barrel.type, this.section._id, applicationType); 29 | } 30 | 31 | public getZeplinLinkType(): ZeplinLinkType { 32 | return ZeplinLinkType.ComponentSection; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/sidebar/zeplinComponent/tree/ZeplinComponentTreeItem.ts: -------------------------------------------------------------------------------- 1 | import TreeItem from "../../../common/vscode/tree/TreeItem"; 2 | import ResponseZeplinComponent from "../../../common/domain/zeplinComponent/model/ResponseZeplinComponent"; 3 | import Barrel from "../../../common/domain/barrel/Barrel"; 4 | import TreeItemContextProvider from "../../../common/vscode/tree/TreeItemContextProvider"; 5 | import TreeItemContext from "../../../common/domain/tree/TreeItemContext"; 6 | import { isComponentPinned } from "../../pin/util/pinUtil"; 7 | import ZeplinUriProvider from "../../../common/domain/openInZeplin/model/ZeplinUriProvider"; 8 | import ApplicationType from "../../../common/domain/openInZeplin/model/ApplicationType"; 9 | import { getComponentUri } from "../../../common/domain/openInZeplin/util/zeplinUris"; 10 | import ZeplinLinkType from "../../../common/domain/openInZeplin/model/ZeplinLinkType"; 11 | 12 | function getContextProvider(zeplinComponent: ResponseZeplinComponent): TreeItemContextProvider { 13 | return new TreeItemContextProvider( 14 | TreeItemContext.ZeplinComponent, 15 | TreeItemContext.ZeplinLink, 16 | isComponentPinned(zeplinComponent) ? TreeItemContext.Pinned : TreeItemContext.Pinnable 17 | ); 18 | } 19 | 20 | export default class ZeplinComponentTreeItem extends TreeItem implements ZeplinUriProvider { 21 | public constructor( 22 | public zeplinComponent: ResponseZeplinComponent, public barrel: Barrel, parent: TreeItem | undefined 23 | ) { 24 | super(zeplinComponent.name, parent, getContextProvider(zeplinComponent)); 25 | this.setRemoteIconPath(zeplinComponent.latestVersion.snapshot.url); 26 | } 27 | 28 | public getZeplinUri(applicationType: ApplicationType): string { 29 | return getComponentUri(this.barrel.id, this.barrel.type, this.zeplinComponent._id, applicationType); 30 | } 31 | 32 | public getZeplinLinkType(): ZeplinLinkType { 33 | return ZeplinLinkType.Component; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/sidebar/zeplinComponent/tree/ZeplinComponentsTreeItem.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import Barrel from "../../../common/domain/barrel/Barrel"; 3 | import TreeItem from "../../../common/vscode/tree/TreeItem"; 4 | import CumulativeBarrelDetailsStore from "../data/CumulativeBarrelDetailsStore"; 5 | import ExpandedErrorTreeItem from "../../../common/vscode/tree/ExpandedErrorTreeItem"; 6 | import BarrelZeplinComponentsTreeItem from "./BarrelZeplinComponentsTreeItem"; 7 | import { createList } from "./zeplinComponentTreeUtil"; 8 | import localization from "../../../localization"; 9 | 10 | export default class ZeplinComponentsTreeItem extends TreeItem { 11 | public constructor(private barrel: Barrel, parent: TreeItem | undefined) { 12 | super( 13 | localization.common.zeplinComponent.zeplinComponents, 14 | parent, 15 | undefined, 16 | vscode.TreeItemCollapsibleState.Collapsed 17 | ); 18 | } 19 | 20 | public async getChildren(): Promise { 21 | const { data, errors } = await new CumulativeBarrelDetailsStore(this.barrel.id, this.barrel.type).get(); 22 | 23 | if (!data?.length) { 24 | const error = errors![0]; 25 | return [new TreeItem(error.message, this)]; 26 | } else if (data.length === 1 && !errors?.length) { 27 | const [{ components }, ...sections] = data[0].componentSections; 28 | return createList(components, sections, this.barrel, this); 29 | } else { 30 | const error = errors?.[0]; 31 | const errorItems = error ? [new ExpandedErrorTreeItem(error.id, error.message, this)] : []; 32 | return [ 33 | ...data.map(barrelDetails => new BarrelZeplinComponentsTreeItem(barrelDetails, this)), 34 | ...errorItems 35 | ]; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/sidebar/zeplinComponent/tree/zeplinComponentTreeUtil.ts: -------------------------------------------------------------------------------- 1 | import TreeItem from "../../../common/vscode/tree/TreeItem"; 2 | import ZeplinComponentSection from "../../../common/domain/zeplinComponent/model/ZeplinComponentSection"; 3 | import ResponseZeplinComponent from "../../../common/domain/zeplinComponent/model/ResponseZeplinComponent"; 4 | import ZeplinComponentTreeItem from "./ZeplinComponentTreeItem"; 5 | import ZeplinComponentSectionTreeItem from "./ZeplinComponentSectionTreeItem"; 6 | import localization from "../../../localization"; 7 | import Barrel from "../../../common/domain/barrel/Barrel"; 8 | 9 | function createList( 10 | components: ResponseZeplinComponent[], sections: ZeplinComponentSection[], barrel: Barrel, parent: TreeItem): 11 | TreeItem[] { 12 | const filledSections = sections?.filter(isSectionFilled) ?? []; 13 | if (components.length || filledSections.length) { 14 | return [ 15 | ...components.map(component => new ZeplinComponentTreeItem(component, barrel, parent)), 16 | ...filledSections.map(section => new ZeplinComponentSectionTreeItem(section, barrel, parent)) 17 | ]; 18 | } else { 19 | return [new TreeItem(localization.sidebar.zeplinComponent.noneFound, parent)]; 20 | } 21 | } 22 | 23 | function isSectionFilled(section: ZeplinComponentSection): boolean { 24 | return !!section.components.length || section.componentSections.some(isSectionFilled); 25 | } 26 | 27 | export { 28 | createList 29 | }; 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "module": "commonjs", 5 | "target": "es6", 6 | "outDir": "out", 7 | "lib": [ 8 | "es6" 9 | ], 10 | "sourceMap": true, 11 | "rootDir": "src", 12 | "strict": true 13 | }, 14 | "exclude": [ 15 | "node_modules", 16 | ".vscode-test" 17 | ] 18 | } --------------------------------------------------------------------------------