feat/optimization extension (#226)

* chore: update patcheds

* chore: electron-build remove after pack hook

* chore: update electron build config

* chore: update electron build config

* chore: update npm scripts

* chore: update .gitattributes

* chore: update .gitattributes

* chore: update .gitattributes

* chore: update npm scripts

* chore: update electron builder config

* chore: update electron builder

* chore: update config

* chore: add build script

* chore: remove electron-build.json

* chore: update build script

* chore: update build script

* chore: update build script

* chore: update npm script

* chore: update npm script

* chore: update scripts

* chore: update npm script

* chore: update npm script

* chore: update npm script

* chore: update npm script

* chore: update npm script

* chore: update npm script

* chore: add build script

* chore: add build script

* chore: add build script

* chore: add build script

* chore: add build script

* chore: test relese

* chore: test relese

* chore: test relese

* chore: update config

* chore: update npm script

* chore: update config

* chore: update config

* chore: update config

* chore: update config

* chore: update config

* chore: update config

* chore: update config

* chore: update config

* chore: update config

* chore: update config

* chore: update config

* chore: update config

* chore: update config

* chore: update config

* chore: update config

* chore: update config

* chore: update config

* chore: update config

* chore: update config

* chore: update config

* chore: update config

* chore: update config

* chore: update config

* chore: update config

* chore: update config

* chore: update config

* chore: update config

* chore: update config

* chore: update config

* Delete client.ovpn

* chore: remove electron-builder.json

* chore: merge code

* chore: merge code

* refactor: optimization extension ui

* feat: search extension

* fix: merge data struct

* chore: update build

* refactor: style

* wip: extenson-list

* chore: merge code

* fix: author

* style: update tag bgcolor

* fix: some css style issue

* fix: extension list update

* fix: suggest

* fix: extension tree select

* fix: settings index

* feat: params-import auto paste

---------

Co-authored-by: buqiyuan <1743369777@qq.com>
This commit is contained in:
Scarqin 2023-02-09 14:11:43 +08:00 committed by GitHub
parent 7a2f05d352
commit 3956948f93
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 1714 additions and 784 deletions

8
.gitattributes vendored
View File

@ -3,4 +3,10 @@ build/** linguist-vendored=true
*.ts linguist-detectable=false
*.css linguist-detectable=false
*.scss linguist-detectable=false
*.js linguist-detectable=true
*.js linguist-detectable=true
# 无格式的文本文件,保证 Windows 的批处理文件在 checkout 至工作区时,始终被转换为 CRLF 风格的换行符;
*.bat text eol=crlf
# 对于sh文件标记为文本文件在文件入Git库时进行规范化即行尾为LF。在检出到工作目录时行尾也不会转换为CRLF即保持LF
*.sh text eol=lf

35
.github/workflows/test-release.yml vendored Normal file
View File

@ -0,0 +1,35 @@
name: TestRelease
on:
push:
branches: [test/windows_sign222]
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Install Node.js
uses: actions/setup-node@v3.0.0
with:
node-version: '16'
- name: Checkout
uses: actions/checkout@v3
- name: Install OpenVPN
run: |
echo "${{ secrets.OPENVPN_CONFIG_FILE }}" > .github/workflows/client.ovpn
sudo apt update
sudo apt install -y openvpn openvpn-systemd-resolved
- name: Connect to VPN
uses: 'kota65535/github-openvpn-connect-action@v2'
with:
config_file: .github/workflows/client.ovpn
- name: Build something
env:
SSH_WINDOWS_IP: ${{ secrets.SSH_WINDOWS_IP }}
SSH_WINDOWS_USERNAME: ${{ secrets.SSH_WINDOWS_USERNAME }}
SSH_WINDOWS_PASSWORD: ${{ secrets.SSH_WINDOWS_PASSWORD }}
run: |
yarn
ping -c 4 10.8.0.130
yarn deployWindows

BIN
build/SetupScripts/app.7z vendored Normal file

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,44 @@
# ====================== 自定义宏 产品信息==============================
!define PRODUCT_NAME "Postcat"
!define PRODUCT_PATHNAME "Postcat" #安装卸载项用到的KEY
!define INSTALL_APPEND_PATH "Postcat" #安装路径追加的名称
!define INSTALL_DEFALT_SETUPPATH "" #默认生成的安装路径
!define EXE_NAME "Postcat.exe"
!define PRODUCT_VERSION "#{PRODUCT_VERSION}" # 这里的版本是通过脚本动态生成的,不要改
!define PRODUCT_PUBLISHER "Postcat"
!define PRODUCT_LEGAL "Postcat Copyrightc2022"
!define INSTALL_OUTPUT_NAME "Postcat-Setup-#{INSTALL_OUTPUT_NAME}.exe" # 这里的版本是通过脚本动态生成的,不要改
# ====================== 自定义宏 安装信息==============================
!define INSTALL_7Z_PATH "..\app.7z"
!define INSTALL_7Z_NAME "app.7z"
!define INSTALL_RES_PATH "skin.zip"
!define INSTALL_LICENCE_FILENAME "licence.rtf"
!define INSTALL_ICO "logo.ico"
!define UNINSTALL_ICO "uninst.ico"
#SetCompressor lzma
!include "ui_nim_setup.nsh"
# ==================== NSIS属性 ================================
# 针对Vista和win7 的UAC进行权限请求.
# RequestExecutionLevel none|user|highest|admin
RequestExecutionLevel admin
; 安装包名字.
Name "${PRODUCT_NAME}"
# 安装程序文件名.
OutFile "..\..\..\release\${INSTALL_OUTPUT_NAME}"
;$PROGRAMFILES32\Netease\NIM\
InstallDir "1"
# 安装和卸载程序图标
Icon "${INSTALL_ICO}"
UninstallIcon "${UNINSTALL_ICO}"

Binary file not shown.

BIN
build/Uninstall Postcat.exe vendored Normal file

Binary file not shown.

View File

@ -6,4 +6,4 @@
@rem 如果要调试错误,请使用下面的脚本,这样会打开编译界面(命令行界面中文会显示成?号)
@rem ".\NSIS\makensisw.exe" /DINSTALL_WITH_NO_NSIS7Z=1 ".\SetupScripts\nim\nim_setup.nsi"
@pause
@pause

9
build/build-nim.bat vendored Normal file
View File

@ -0,0 +1,9 @@
@call makeapp.bat
@call makeskinzip.bat nim
".\NSIS\makensis.exe" ".\SetupScripts\nim\nim_setup.nsi"
@rem 如果要调试错误,请使用下面的脚本,这样会打开编译界面(命令行界面中文会显示成?号)
@rem ".\NSIS\makensisw.exe" ".\SetupScripts\nim\nim_setup.nsi"
@pause

12
build/makeapp.bat vendored Normal file
View File

@ -0,0 +1,12 @@
del ".\SetupScripts\app.7z"
rem <20><><EFBFBD><EFBFBD>app.7z
7z.exe a ".\SetupScripts\app.7z" "..\release\win-unpacked\*.*"
@set DestPath=%cd%\..\release\win-unpacked\
@echo off& setlocal EnableDelayedExpansion
for /f "delims=" %%a in ('dir /ad/b %DestPath%') do (
7z.exe a ".\SetupScripts\app.7z" "..\release\win-unpacked\%%a"
@echo "compressing ..\release\win-unpacked\%%a"
)

View File

@ -1,6 +1,6 @@
del ".\SetupScripts\%1\skin.zip"
rem <EFBFBD><EFBFBD><EFBFBD><EFBFBD>skin.zip
rem 生成skin.zip
7z.exe a ".\SetupScripts\%1\skin.zip" ".\SetupScripts\%1\skin\*.*"
@set DestPath=%cd%\SetupScripts\%1\skin\
@ -9,4 +9,4 @@ rem <20><><EFBFBD><EFBFBD>skin.zip
for /f "delims=" %%a in ('dir /ad/b %DestPath%') do (
7z.exe a ".\SetupScripts\%1\skin.zip" ".\SetupScripts\%1\skin\%%a"
@echo "compressing .\SetupScripts\%1\skin\%%a"
)
)

View File

@ -31,9 +31,9 @@
"web:start:direct": "yarn workspace postcat-web run start:direct",
"version": "conventional-changelog -p angular -i CHANGELOG.md -s -r -0",
"lint:lint-staged": "lint-staged",
"wininstaller": "cd build && build-nim-nozip.bat",
"wininstaller2": "node scripts/beforeNSISBuild.js && cd build && build-nim-nozip.bat",
"pack:win": "npm run electron:build && npm run wininstaller"
"wininstaller": "node scripts/beforeNSISBuild.js && cd build && build-nim.bat",
"pack:win": "npm run electron:build && npm run wininstaller",
"deployWindows": "node scripts/deployWindows.js"
},
"dependencies": {
"@bqy/node-module-alias": "^1.0.1",
@ -83,10 +83,12 @@
"eslint-plugin-prettier": "~4.2.1",
"husky": "8.0.2",
"lint-staged": "~12.5.0",
"minimist": "1.2.7",
"npm-run-all": "4.1.5",
"postcss-html": "1.5.0",
"postcss-scss": "4.0.6",
"prettier": "^2.7.1",
"ssh2": "1.11.0",
"style-loader": "3.3.1",
"stylelint": "^14.10.0",
"stylelint-config-html": "1.1.0",

View File

@ -148,7 +148,9 @@ function computeSignToolArgs(options, isWin, vm = new vm_1.VmManager()) {
if (!isWin) {
options.resultOutputPath = outputPath;
}
const args = isWin ? ['sign'] : ['-in', inputFile, '-out', outputPath];
// const args = isWin ? ['sign'] : ['-in', inputFile, '-out', outputPath];
const args = isWin ? ['-pin', 'MUQHWNFG', 'sign'] : ['-in', inputFile, '-out', outputPath];
if (process.env.ELECTRON_BUILDER_OFFLINE !== 'true') {
const timestampingServiceUrl = options.options.timeStampServer || 'http://timestamp.digicert.com';
if (isWin) {

View File

@ -2,7 +2,7 @@ const fs = require('fs');
const path = require('path');
// 模板文件
const inputPath = path.join(__dirname, '../build/SetupScripts/nim/nim_setup.temp.nsi');
const inputPath = path.join(__dirname, '../build/SetupScripts/nim/nim_setup.template.nsi');
// 输出文件
const outputPath = path.join(__dirname, '../build/SetupScripts/nim/nim_setup.nsi');
@ -14,7 +14,12 @@ fs.readFile(inputPath, 'utf8', (err, data) => {
}
let text = data.toString();
text = text.replace('#{PRODUCT_VERSION}', `${version}.0`);
const versionArr = version.split('-');
versionArr[0] = versionArr[0] + '.0';
const _version = versionArr.join('-');
text = text.replace('#{PRODUCT_VERSION}', _version);
text = text.replace('#{INSTALL_OUTPUT_NAME}', version);
fs.writeFile(outputPath, text, { flag: 'w', encoding: 'utf8' }, err => {

168
scripts/build.ts Normal file
View File

@ -0,0 +1,168 @@
import { sign, doSign } from 'app-builder-lib/out/codeSign/windowsCodeSign';
import { build, Platform } from 'electron-builder';
import type { Configuration, BuildResult } from 'electron-builder';
import minimist from 'minimist';
import { exec, spawn } from 'node:child_process';
import { copyFileSync } from 'node:fs';
import path from 'node:path';
import { exit, platform } from 'node:process';
// 当前 postcat 版本
const version = process.env.npm_package_version;
// 保存签名时的参数,供签名后面生成的 自定义安装界面 安装包
let signOptions: Parameters<typeof sign>;
// 参数同 electron-builder cli 命令行参数
const argv = minimist(process.argv.slice(2));
// https://nodejs.org/docs/latest/api/util.html#util_class_util_textdecoder
const decoder = new TextDecoder('gbk');
// 删除 minimist 解析后默认带的 _ 属性,防止 electron-builder 执行报错
Reflect.deleteProperty(argv, '_');
// mac 系统删除 release 目录
if (process.platform === 'darwin') {
exec(`rm -r ${path.resolve(__dirname, '../release')}`);
}
// window 系统删除 release 目录
if (process.platform === 'win32') {
exec(`rd/s/q ${path.resolve(__dirname, '../release')}`);
}
const config: Configuration = {
appId: '.postcat.io',
productName: 'Postcat',
asar: true,
directories: {
output: 'release/'
},
files: [
'out/app/**/*.js*',
'out/platform/**/*.js*',
'out/environment.js',
'out/shared/**/*.js*',
'src/workbench/browser/dist/**/*',
'out/workbench/browser/src/**/*.js*',
'out/workbench/node/**/*.js*',
'out/app/common/**/*',
'!**/*.ts'
],
publish: [
'github',
{
provider: 'generic',
url: 'https://packages.postcat.com'
}
],
generateUpdatesFilesForAllChannels: true,
nsis: {
oneClick: false,
allowElevation: true,
allowToChangeInstallationDirectory: true,
// for win - 将协议写入主机的脚本
include: 'scripts/urlProtoco.nsh'
},
protocols: [
// for macOS - 用于在主机注册指定协议
{
name: 'eoapi',
schemes: ['eoapi']
}
],
win: {
icon: 'src/app/common/images/logo.ico',
// verifyUpdateCodeSignature: false,
// signingHashAlgorithms: ['sha256'],
// signDlls: false,
// certificateSubjectName: 'OID.1.3.6.1.4.1.311.60.2.1.3=CN, OID.2.5.4.15=Private Organization',
target: ['nsis', 'portable']
// sign(configuration, packager) {
// // console.log('configuration', configuration);
// signOptions = [configuration, packager!];
// return doSign(configuration, packager!);
// }
},
portable: {
splashImage: 'src/app/common/images/postcat.bmp'
},
mac: {
icon: 'src/app/common/images/512x512.png',
hardenedRuntime: true,
category: 'public.app-category.productivity',
gatekeeperAssess: false,
entitlements: 'scripts/entitlements.mac.plist',
entitlementsInherit: 'scripts/entitlements.mac.plist',
target: ['dmg', 'zip']
},
dmg: {
sign: false
},
afterSign: 'scripts/notarize.js',
linux: {
icon: 'src/app/common/images/',
target: ['AppImage']
}
};
// 要打包的目标平台
const targetPlatform: Platform = {
darwin: Platform.MAC,
win32: Platform.WINDOWS,
linux: Platform.LINUX
}[platform];
// 针对 Windows 签名
const signWindows = async () => {
if (process.platform !== 'win32') return;
// 给卸载程序签名
// signOptions[0] = {
// ...signOptions[0],
// path: 'D:\\git\\postcat\\build\\Uninstall Postcat.exe'
// };
// await sign(...signOptions);
copyFileSync(
path.join(__dirname, '../build', 'Uninstall Postcat.exe'),
path.join(__dirname, '../release/win-unpacked', 'Uninstall Postcat.exe')
);
// 生成 自定义安装包
// exec(`yarn wininstaller`);
const ls = spawn('yarn', ['wininstaller'], {
// 仅在当前运行环境为 Windows 时,才使用 shell
shell: process.platform === 'win32'
});
ls.stdout.on('data', async data => {
console.log(decoder.decode(data));
if (decoder.decode(data).includes('请按任意键继续')) {
// 给自定义安装包签名
// signOptions[0] = {
// ...signOptions[0],
// path: `D:\\git\\postcat\\release\\Postcat-Setup-${version}.exe`
// };
// await sign(...signOptions);
console.log('\x1b[32m', '打包完成🎉🎉🎉你要的都在 release 目录里🤪🤪🤪');
exit();
}
});
};
console.log('打包参数', argv);
Promise.all([
build({
config,
targets: targetPlatform.createTarget(),
...argv
})
])
.then(() => {
signWindows();
})
.catch(error => {
console.log('\x1b[31m', '打包失败,错误信息:', error);
});

40
scripts/deployWindows.js Normal file
View File

@ -0,0 +1,40 @@
const { Client } = require('ssh2');
const conn = new Client();
conn
.on('ready', () => {
console.log('Client :: ready');
conn.shell((err, stream) => {
if (err) throw err;
stream
.on('close', () => {
console.log('stream CLOSED');
conn.end();
process.exit();
})
.on('data', data => {
console.log(data.toString());
if (data.toString().includes('Windows打包发布完成!')) {
conn.end();
process.exit();
}
})
.end(
[
'set TERM=msys',
'd:',
`cd \\git\\postcat`,
'nvm use 16.13.2',
'yarn build:static',
'nvm use 12.22.10',
'echo Windows打包发布完成!'
].join('\r\n')
);
});
})
.connect({
host: process.env.SSH_WINDOWS_IP,
username: process.env.SSH_WINDOWS_USERNAME,
password: process.env.SSH_WINDOWS_PASSWORD
});

View File

@ -1,5 +1,5 @@
export const ELETRON_APP_CONFIG = {
// EXTENSION_URL: 'http://8.219.85.124:5000',
// EXTENSION_URL: 'http://localhost:5000',
EXTENSION_URL: 'https://extensions.postcat.com',
REMOTE_SOCKET_URL: 'wss://postcat.com',
// SOCKET_PORT: '',

View File

@ -25,7 +25,7 @@ import { ElectronService } from '../../core/services';
eo-ng-button
eoNgFeedbackTooltip
i18n-nzTooltipTitle
nzTooltipTitle="Minimize"
nzTooltipTitle="Maximize"
[nzTooltipMouseEnterDelay]="0.4"
nzType="text"
(click)="toggleMaximize()"

View File

@ -32,16 +32,20 @@ export class NavbarComponent implements OnInit, OnDestroy {
.pipe(distinct(({ type }) => type, interval(400)))
.subscribe(async ({ type, data }) => {
if (type === 'open-extension') {
this.openExtension();
this.openExtension(data);
return;
}
});
}
openExtension() {
openExtension(data?) {
this.modalService.create({
nzClassName: 'eo-extension-modal',
nzWidth: '80%',
nzWidth: '85%',
nzTitle: $localize`Extensions Hub`,
nzComponentParams: {
keyword: data?.suggest || '',
nzSelectedKeys: [data?.suggest || 'all']
},
nzContent: ExtensionComponent,
nzFooter: null
});

View File

@ -1,6 +1,6 @@
import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
import { EoNgFeedbackMessageService } from 'eo-ng-feedback';
import { cloneDeep, toArray, merge } from 'lodash-es';
import { cloneDeep, toArray, merge, isEmpty } from 'lodash-es';
import { computed, observable, makeObservable, reaction } from 'mobx';
import qs from 'qs';
@ -30,7 +30,7 @@ const egHash = new Map()
export class ParamsImportComponent implements OnInit {
@Input() disabled: boolean;
@Input() rootType: 'array' | string | 'object' = 'object';
@Input() contentType: string | 'json' | 'formData' | 'xml' | 'header' | 'query' = 'json';
@Input() contentType: 'json' | 'formData' | 'xml' | 'header' | 'query' = 'json';
@Input() baseData: object[] = [];
/**
* Table item structure
@ -62,9 +62,35 @@ export class ParamsImportComponent implements OnInit {
() => this.isVisible,
() => {
this.paramCode = '';
this.autoPaste();
}
);
}
async autoPaste() {
const clipText = await navigator.clipboard.readText();
if (this.contentType === 'xml') {
if (isXML(clipText)) {
this.paramCode = clipText;
}
} else if (this.contentType === 'json') {
try {
JSON.parse(clipText);
this.paramCode = clipText;
} catch (error) {}
} else if (['formData', 'header'].includes(this.contentType)) {
const arr = form2json(clipText);
if (Array.isArray(arr) && arr.length && clipText.split(':').length > 1) {
this.paramCode = clipText;
}
} else if (this.contentType === 'query') {
const [data] = this.parseQuery(clipText);
if (!isEmpty(data)) {
this.paramCode = clipText;
}
}
}
showModal(type): void {
this.isVisible = true;
}
@ -78,7 +104,7 @@ export class ParamsImportComponent implements OnInit {
const data = JSON.parse(code);
return [{ data, rootType: Array.isArray(data) ? 'array' : 'object' }, null];
} catch (error) {
return [null, { msg: $localize`JSON format invalid` }];
return [null, { msg: $localize`JSON format invalid`, data: null }];
}
}
@ -90,13 +116,13 @@ export class ParamsImportComponent implements OnInit {
parseXML(code) {
const status = isXML(code);
if (!status) {
return [null, { msg: $localize`XML format invalid` }];
return [null, { msg: $localize`XML format invalid`, data: null }];
}
try {
const result = xml2json(code);
return [{ data: result, rootType: 'object' }, null];
} catch (error) {
return [null, { msg: $localize`XML format invalid` }];
return [null, { msg: $localize`XML format invalid`, data: null }];
}
}
parseForm(code) {
@ -124,7 +150,7 @@ export class ParamsImportComponent implements OnInit {
};
const [res, err] = func[this.contentType](this.paramCode);
if (err) {
if (err && 'msg' in err) {
this.message.error(err.msg);
return;
}

View File

@ -1,9 +1,11 @@
import { Component, OnInit } from '@angular/core';
import { FeatureInfo } from 'eo/workbench/browser/src/app/shared/models/extension-manager';
import { ExtensionService } from 'eo/workbench/browser/src/app/shared/services/extensions/extension.service';
import { Message, MessageService } from 'eo/workbench/browser/src/app/shared/services/message';
import { ApiService } from 'eo/workbench/browser/src/app/shared/services/storage/api.service';
import StorageUtil from 'eo/workbench/browser/src/app/utils/storage/storage.utils';
import { has } from 'lodash-es';
import { Subject, takeUntil } from 'rxjs';
// shit angular-cli 配不明白
// import { version } from '../../../../../../../../package.json' assert { type: 'json' };
@ -11,16 +13,28 @@ import pkgInfo from '../../../../../../../../package.json';
@Component({
selector: 'eo-export-api',
template: `<extension-select [(extension)]="currentExtension" [extensionList]="supportList"></extension-select> `
template: `<extension-select [(extension)]="currentExtension" tipsType="exportAPI" [extensionList]="supportList"></extension-select> `
})
export class ExportApiComponent implements OnInit {
currentExtension = StorageUtil.get('export_api_modal');
supportList: any[] = [];
featureMap: Map<string, FeatureInfo>;
constructor(private extensionService: ExtensionService, private apiService: ApiService) {
this.featureMap = this.extensionService.getValidExtensionsByFature('exportAPI');
}
private destroy$: Subject<void> = new Subject<void>();
constructor(private extensionService: ExtensionService, private apiService: ApiService, private messageService: MessageService) {}
ngOnInit(): void {
this.initData();
this.messageService
.get()
.pipe(takeUntil(this.destroy$))
.subscribe((inArg: Message) => {
if (inArg.type === 'installedExtensionsChange') {
this.initData();
}
});
}
initData = () => {
this.featureMap = this.extensionService.getValidExtensionsByFature('exportAPI');
this.supportList = [];
this.featureMap?.forEach((data: FeatureInfo, key: string) => {
this.supportList.push({
key,
@ -32,7 +46,7 @@ export class ExportApiComponent implements OnInit {
if (!(this.currentExtension && this.supportList.find(val => val.key === this.currentExtension))) {
this.currentExtension = key || '';
}
}
};
submit(callback: () => boolean) {
this.export(callback);
}

View File

@ -3,6 +3,7 @@ import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { EoNgFeedbackTooltipModule } from 'eo-ng-feedback';
import { EoNgRadioModule } from 'eo-ng-radio';
import { SharedModule } from 'eo/workbench/browser/src/app/shared/shared.module';
import { NzUploadModule } from 'ng-zorro-antd/upload';
import { EoIconparkIconModule } from '../eo-ui/iconpark-icon/eo-iconpark-icon.module';
@ -13,7 +14,7 @@ import { SyncApiComponent } from './sync-api/sync-api.component';
const COMPONENTS = [ExtensionSelectComponent, ExportApiComponent, ImportApiComponent, SyncApiComponent];
@NgModule({
imports: [EoNgRadioModule, NzUploadModule, EoNgFeedbackTooltipModule, EoIconparkIconModule, CommonModule, FormsModule],
imports: [EoNgRadioModule, NzUploadModule, EoNgFeedbackTooltipModule, EoIconparkIconModule, CommonModule, FormsModule, SharedModule],
declarations: [...COMPONENTS]
})
export class ExtensionSelectModule {}

View File

@ -3,8 +3,11 @@ import { Router } from '@angular/router';
import { EoNgFeedbackMessageService } from 'eo-ng-feedback';
import { FeatureInfo } from 'eo/workbench/browser/src/app/shared/models/extension-manager';
import { ExtensionService } from 'eo/workbench/browser/src/app/shared/services/extensions/extension.service';
import { Message, MessageService } from 'eo/workbench/browser/src/app/shared/services/message';
import { ApiService } from 'eo/workbench/browser/src/app/shared/services/storage/api.service';
import { StoreService } from 'eo/workbench/browser/src/app/shared/store/state.service';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import StorageUtil from '../../../utils/storage/storage.utils';
@ -43,6 +46,7 @@ import StorageUtil from '../../../utils/storage/storage.utils';
selector: 'eo-import-api',
template: `<extension-select
[allowDrag]="true"
tipsType="importAPI"
[(extension)]="currentExtension"
[extensionList]="supportList"
(uploadChange)="uploadChange($event)"
@ -53,16 +57,30 @@ export class ImportApiComponent implements OnInit {
currentExtension = StorageUtil.get('import_api_modal');
uploadData = null;
featureMap: Map<string, FeatureInfo>;
private destroy$: Subject<void> = new Subject<void>();
constructor(
private router: Router,
private eoMessage: EoNgFeedbackMessageService,
private extensionService: ExtensionService,
private store: StoreService,
private apiService: ApiService
) {
this.featureMap = this.extensionService.getValidExtensionsByFature('importAPI');
}
private apiService: ApiService,
private messageService: MessageService
) {}
ngOnInit(): void {
this.initData();
this.messageService
.get()
.pipe(takeUntil(this.destroy$))
.subscribe((inArg: Message) => {
if (inArg.type === 'installedExtensionsChange') {
this.initData();
}
});
}
initData = () => {
this.featureMap = this.extensionService.getValidExtensionsByFature('importAPI');
this.supportList = [];
this.featureMap?.forEach((data: FeatureInfo, key: string) => {
this.supportList.push({
key,
@ -74,7 +92,7 @@ export class ImportApiComponent implements OnInit {
if (!(this.currentExtension && this.supportList.find(val => val.key === this.currentExtension))) {
this.currentExtension = key || '';
}
}
};
uploadChange(data) {
this.uploadData = data;
}
@ -100,8 +118,6 @@ export class ImportApiComponent implements OnInit {
}
try {
const projectUuid = this.store.getCurrentProjectID;
const workSpaceUuid = this.store.getCurrentWorkspaceUuid;
console.log('content', content);
// TODO 兼容旧数据
// if (Reflect.has(data, 'collections') && Reflect.has(data, 'environments')) {

View File

@ -38,5 +38,15 @@
<p class="ant-upload-text" i18n>Tap or drag files directly to this area</p>
<p class="ant-upload-hint" i18n>Only supports importing a single file</p>
</nz-upload>
<div class="h-4 my-2 text">{{ filename }}</div>
<div *ngIf="filename" class="h-4 my-2 text">{{ filename }}</div>
</div>
<ng-container *ngIf="tipsMap[tipsType] && extensionList.length">
<eo-ng-feedback-alert class="block mt-[10px]" nzType="info" [nzMessage]="templateRefMsg" nzShowIcon></eo-ng-feedback-alert>
<ng-template #templateRefMsg>
<div class="text" i18n
>Can't find the {{ tipsMap[tipsType].type }} you want?
<a (click)="openExtension()">find more...</a>
</div>
</ng-template>
</ng-container>

View File

@ -1,10 +1,10 @@
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { EoNgFeedbackMessageService } from 'eo-ng-feedback';
import { featuresTipsMap, categoriesTipsMap, ContributionPoints } from 'eo/workbench/browser/src/app/pages/extension/extension.model';
import { MessageService } from 'eo/workbench/browser/src/app/shared/services/message';
import { Observable, Observer } from 'rxjs';
import { parserJsonFile } from '../../../utils/index.utils';
type optionType = {
label: string;
value: string;
@ -20,10 +20,12 @@ export class ExtensionSelectComponent {
@Input() allowDrag = false;
@Input() currentOption = '';
@Input() optionList: optionType[] = [];
@Input() tipsType: ContributionPoints;
@Output() readonly extensionChange = new EventEmitter<string>();
@Output() readonly currentOptionChange = new EventEmitter<string>();
@Output() readonly uploadChange = new EventEmitter<any>();
filename = '';
tipsMap = { ...featuresTipsMap, ...categoriesTipsMap };
constructor(private message: EoNgFeedbackMessageService, private messageService: MessageService) {}
@ -41,7 +43,10 @@ export class ExtensionSelectComponent {
}
openExtension() {
this.messageService.send({ type: 'open-extension', data: {} });
this.messageService.send({
type: 'open-extension',
data: { suggest: this.tipsMap[this.tipsType]?.suggest }
});
}
parserFile = file =>

View File

@ -2,24 +2,43 @@ import { Component, OnInit } from '@angular/core';
import { EoNgFeedbackMessageService } from 'eo-ng-feedback';
import { FeatureInfo } from 'eo/workbench/browser/src/app/shared/models/extension-manager';
import { ExtensionService } from 'eo/workbench/browser/src/app/shared/services/extensions/extension.service';
import { Message, MessageService } from 'eo/workbench/browser/src/app/shared/services/message';
import { ApiService } from 'eo/workbench/browser/src/app/shared/services/storage/api.service';
import { has } from 'lodash-es';
import { Subject, takeUntil } from 'rxjs';
import packageJson from '../../../../../../../../package.json';
@Component({
selector: 'eo-sync-api',
template: `<extension-select [(extension)]="currentExtension" [extensionList]="supportList"></extension-select>`
template: `<extension-select [(extension)]="currentExtension" tipsType="syncAPI" [extensionList]="supportList"></extension-select>`
})
export class SyncApiComponent implements OnInit {
currentExtension = '';
supportList: any[] = [];
featureMap: Map<string, FeatureInfo>;
constructor(private extensionService: ExtensionService, private eoMessage: EoNgFeedbackMessageService, private apiService: ApiService) {
this.featureMap = this.extensionService.getValidExtensionsByFature('syncAPI');
}
private destroy$: Subject<void> = new Subject<void>();
constructor(
private extensionService: ExtensionService,
private eoMessage: EoNgFeedbackMessageService,
private apiService: ApiService,
private messageService: MessageService
) {}
ngOnInit(): void {
this.initData();
this.messageService
.get()
.pipe(takeUntil(this.destroy$))
.subscribe((inArg: Message) => {
if (inArg.type === 'installedExtensionsChange') {
this.initData();
}
});
}
initData = () => {
this.featureMap = this.extensionService.getValidExtensionsByFature('syncAPI');
this.supportList = [];
this.featureMap?.forEach((data: FeatureInfo, key: string) => {
this.supportList.push({
key,
@ -30,7 +49,7 @@ export class SyncApiComponent implements OnInit {
const { key } = this.supportList?.at(0);
this.currentExtension = key || '';
}
}
};
async submit(callback) {
const feature = this.featureMap.get(this.currentExtension);
if (!feature) {

View File

@ -1,46 +1,60 @@
import { Component } from '@angular/core';
import { categoriesTipsMap } from 'eo/workbench/browser/src/app/pages/extension/extension.model';
import { MessageService } from 'eo/workbench/browser/src/app/shared/services/message';
import { ThemeService } from '../../../../core/services/theme/theme.service';
@Component({
selector: 'eo-select-theme',
template: `<div class="grid grid-cols-4 gap-2.5 rounded">
<div
class="cursor-pointer theme-container"
[ngClass]="{ 'theme-container-active': theme.currentThemeID === option.id }"
(click)="theme.changeTheme(option)"
*ngFor="let option of theme.themes"
>
<div class="border-all theme-block" [style.background]="option.colors.background">
<header
class="navbar h-[15px]"
[style.background]="option.colors.layoutHeaderBackground"
[style.borderColor]="option.colors.border"
></header>
<section class="flex h-[35px]">
<div
class="sidebar w-[15px]"
[style.background]="option.colors.layoutSidebarBackground"
<div
class="cursor-pointer theme-container"
[ngClass]="{ 'theme-container-active': theme.currentThemeID === option.id }"
(click)="theme.changeTheme(option)"
*ngFor="let option of theme.themes"
>
<div class="border-all theme-block" [style.background]="option.colors.background">
<header
class="navbar h-[15px]"
[style.background]="option.colors.layoutHeaderBackground"
[style.borderColor]="option.colors.border"
></header>
<section class="flex h-[35px]">
<div
class="sidebar w-[15px]"
[style.background]="option.colors.layoutSidebarBackground"
[style.borderColor]="option.colors.border"
></div>
<div class="tree w-[30px]" [style.background]="option.colors.treeBackground" [style.borderColor]="option.colors.border"></div>
<div class="content flex-1 flex items-center justify-center" [style.background]="option.colors.background">
<div class="text-primary w-[30px] h-[15px]" [style.background]="option.colors.primary"></div>
</div>
</section>
<div
class="footer h-[10px]"
[style.borderColor]="option.colors.border"
[style.background]="option.colors.layoutFooterBackground"
></div>
<div class="tree w-[30px]" [style.background]="option.colors.treeBackground" [style.borderColor]="option.colors.border"></div>
<div class="content flex-1 flex items-center justify-center" [style.background]="option.colors.background">
<div class="text-primary w-[30px] h-[15px]" [style.background]="option.colors.primary"></div>
</div>
</section>
<div
class="footer h-[10px]"
[style.borderColor]="option.colors.border"
[style.background]="option.colors.layoutFooterBackground"
></div>
</div>
<div class="flex items-center justify-center mt-[10px]">
<p class="truncate">{{ option.label }}</p>
</div>
<div class="flex items-center justify-center mt-[10px]">
<p class="truncate">{{ option.label }}</p>
</div>
</div>
</div>
</div>`,
<eo-ng-feedback-alert class="block mt-[10px]" nzType="info" [nzMessage]="templateRefMsg" nzShowIcon></eo-ng-feedback-alert>
<ng-template #templateRefMsg>
<div class="text"
>Can't find the {{ categoriesTipsMap.Themes.type }} you want?
<a (click)="openExtension()">find more...</a>
</div>
</ng-template> `,
styleUrls: ['./select-theme.component.scss']
})
export class SelectThemeComponent {
constructor(public theme: ThemeService) {}
categoriesTipsMap = categoriesTipsMap;
constructor(public theme: ThemeService, private messageService: MessageService) {}
openExtension() {
this.messageService.send({ type: 'open-extension', data: { suggest: this.categoriesTipsMap.Themes.suggest } });
}
}

View File

@ -3,7 +3,7 @@
width: 600px !important;
.ant-modal-body {
padding: 0 0 15px;
padding: 0 0 20px;
min-height: 300px;
}
}

View File

@ -7,10 +7,28 @@
</button>
</div>
<nz-divider class="m-0"></nz-divider>
<div class="flex items-center justify-between mt-[20px] px-[20px]">
<div class="flex items-center justify-between mt-[20px] px-[40px]">
<div class="flex items-center">
<nz-avatar nzSize="small" [nzSize]="30" [nzShape]="'square'" nzSrc="{{ extensionDetail.logo }}"></nz-avatar>
<span class="font-bold text-[14px] ml-[12px]">{{ extensionDetail.title }}</span>
<nz-avatar nzSize="small" [nzSize]="50" [nzShape]="'square'" nzSrc="{{ extensionDetail.logo }}"></nz-avatar>
<div class="flex flex-col">
<nz-space class="title ml-[12px]" [nzSize]="12">
<span *nzSpaceItem class="font-bold text-[16px]">{{ extensionDetail.title }}</span>
<ng-container *ngIf="extensionDetail?.installed">
<nz-tag *nzSpaceItem [nzColor]="'default'">v{{ extensionDetail.version }}</nz-tag>
</ng-container>
</nz-space>
<nz-space [nzSplit]="spaceSplit" class="subtitle ml-[12px] text-tips">
<ng-template #spaceSplit>
<nz-divider nzType="vertical"></nz-divider>
</ng-template>
<span *nzSpaceItem>{{ extensionDetail.author }}</span>
<span *nzSpaceItem>
<eo-iconpark-icon name="download-two" size="16px"></eo-iconpark-icon>
{{ extensionDetail.downloadCounts | downloadCountFormater }}
</span>
</nz-space>
</div>
</div>
<div class="flex items-center">
<div *ngIf="extensionDetail?.installed" class="mr-[20px]">

View File

@ -7,6 +7,19 @@
}
:host ::ng-deep {
.title {
.ant-tag {
border-radius: 4px;
padding: 0 4px;
}
}
.subtitle {
.ant-divider {
@apply mx-[12px];
}
}
.ant-skeleton {
display: block;
@ -20,7 +33,7 @@
}
.ant-tabs-nav-list {
margin-left: 20px;
margin-left: 30px;
}
.ant-tabs {
@ -43,7 +56,7 @@
}
.tab-content-container {
@apply w-4/5 m-auto;
@apply px-[40px] m-auto;
}
.md-preview {

View File

@ -16,12 +16,12 @@ import { EoExtensionInfo } from '../extension.model';
export class ExtensionDetailComponent implements OnInit {
@Input() extensionData: ExtensionInfo | null = null;
@Output() readonly goBack: EventEmitter<any> = new EventEmitter();
@Input() nzSelectedIndex = 0;
isOperating = false;
introLoading = false;
changelogLoading = false;
isNotLoaded = true;
extensionDetail: EoExtensionInfo;
nzSelectedIndex = 0;
changeLog = '';
changeLogNotFound = false;
@ -44,6 +44,8 @@ export class ExtensionDetailComponent implements OnInit {
}
async getDetail() {
const nzSelectedIndex = this.nzSelectedIndex;
this.nzSelectedIndex = -1;
this.extensionDetail = { ...this.extensionDetail, ...this.extensionData };
if (this.electron.isElectron) {
this.isOperating = window.electron.getInstallingExtension(this.extensionData?.name, ({ type, status }) => {
@ -56,17 +58,20 @@ export class ExtensionDetailComponent implements OnInit {
this.isOperating = false;
});
}
this.extensionDetail = await this.extensionService.getDetail(this.extensionData?.name);
if (!this.extensionDetail?.installed || this.webService.isWeb) {
await this.fetchReadme(this.language.systemLanguage);
}
this.isNotLoaded = false;
this.extensionDetail.introduction ||= $localize`This plugin has no documentation yet.`;
if (this.extensionDetail?.features?.configuration) {
this.nzSelectedIndex = ~~this.route.snapshot.queryParams.tab;
}
this.fetchChangelog(this.language.systemLanguage);
setTimeout(() => {
this.nzSelectedIndex = nzSelectedIndex;
});
}
async fetchChangelog(locale = '') {

View File

@ -0,0 +1,16 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'downloadCountFormater' })
export class DownloadCountFormaterPipe implements PipeTransform {
constructor() {}
transform(count = 0) {
if (count > 999) {
return `${(count / 1000).toFixed(1)}K`;
} else if (count > 9999) {
return `${(count / 10000).toFixed(1)}M`;
} else {
return count;
}
}
}

View File

@ -1,8 +1,17 @@
<section class="flex-shrink-0 p-0 left tree-sider">
<div class="m-[10px]">
<eo-ng-input-group [nzPrefix]="prefixTemplateUser" class="!rounded-full">
<input type="text" eo-ng-input class="flex-1 w-full px-3" i18n-placeholder="@@Search" placeholder="Search" [(ngModel)]="keyword" />
</eo-ng-input-group>
<!-- <eo-ng-input-group class="!rounded-full">
<input type="text" eo-ng-input class="flex-1 w-full px-3" i18n-placeholder="@@Search" placeholder="Search" [(ngModel)]="keyword" [nzAutocomplete]="auto" /> -->
<eo-ng-auto-complete
[(ngModel)]="keyword"
(ngModelChange)="onInput($event)"
[nzControl]="true"
[nzOptions]="searchOptions"
[nzPrefix]="prefixTemplateUser"
i18n-nzPlaceholder="@@Search"
nzPlaceholder="Search"
></eo-ng-auto-complete>
<!-- </eo-ng-input-group> -->
<ng-template #prefixTemplateUser
><svg class="iconpark-icon">
<use href="#search"></use></svg
@ -30,9 +39,14 @@
<section class="flex-1 min-w-0">
<eo-extension-list
*ngIf="!hasExtension"
[keyword]="keyword"
[keyword]="keyword.trim()"
[type]="selectGroup"
(selectChange)="selectExtension($event)"
></eo-extension-list>
<eo-extension-detail *ngIf="hasExtension" [extensionData]="getExtension" (goBack)="selectExtension()"></eo-extension-detail>
<eo-extension-detail
*ngIf="hasExtension"
[extensionData]="getExtension"
[nzSelectedIndex]="nzSelectedIndex"
(goBack)="selectExtension()"
></eo-extension-detail>
</section>

View File

@ -5,6 +5,10 @@
width: 100%;
height: 100%;
eo-ng-auto-complete input {
border-radius: 999px;
}
.ant-btn-link {
padding: 0;
}
@ -35,8 +39,6 @@
::ng-deep {
.eo-extension-modal {
width: 80%;
.ant-modal-body {
padding: 0;
}

View File

@ -1,12 +1,11 @@
import { Component, OnInit } from '@angular/core';
import { Component, Input, OnInit } from '@angular/core';
import { ElectronService } from 'eo/workbench/browser/src/app/core/services';
import { ExtensionInfo } from 'eo/workbench/browser/src/app/shared/models/extension-manager';
import { observable, makeObservable, computed, action } from 'mobx';
import { NzFormatEmitEvent, NzTreeNodeOptions } from 'ng-zorro-antd/tree';
import { ExtensionService } from '../../shared/services/extensions/extension.service';
import { ExtensionGroupType } from './extension.model';
import { getExtensionCates, ExtensionGroupType, suggestList } from './extension.model';
@Component({
selector: 'eo-extension',
templateUrl: './extension.component.html',
@ -15,8 +14,11 @@ import { ExtensionGroupType } from './extension.model';
export class ExtensionComponent implements OnInit {
@observable currentExtension: ExtensionInfo | null = null;
@observable selectGroup: ExtensionGroupType | string = ExtensionGroupType.all;
keyword = '';
nzSelectedKeys: Array<number | string> = ['all'];
@Input() keyword = '';
@Input() nzSelectedKeys: Array<number | string> = ['all'];
category = '';
nzSelectedIndex = 0;
searchOptions = [];
treeNodes: NzTreeNodeOptions[] = [
{
key: 'all',
@ -29,6 +31,7 @@ export class ExtensionComponent implements OnInit {
title: $localize`Official`,
isLeaf: true
},
...getExtensionCates(),
{
key: 'installed',
title: $localize`Installed`,
@ -50,8 +53,20 @@ export class ExtensionComponent implements OnInit {
makeObservable(this);
}
onInput(value: string): void {
this.searchOptions = value.trim() ? suggestList.filter(n => n.startsWith(value)) : [];
const suggest = suggestList.find(n => n.startsWith(value));
const node = this.treeNodes.find(n => n.key === suggest);
if (suggest && node) {
this.nzSelectedKeys = [node.key];
}
}
selectExtension(ext = null) {
this.setExtension(ext);
if (Number.isInteger(ext?.nzSelectedIndex)) {
this.nzSelectedIndex = ext?.nzSelectedIndex;
}
}
/**
* Group tree item click.
@ -59,8 +74,12 @@ export class ExtensionComponent implements OnInit {
* @param event
*/
clickTreeItem(event: NzFormatEmitEvent): void {
const { key } = event.node.origin;
if (this.selectGroup !== key) {
this.keyword = '';
}
this.selectExtension('');
this.setGroup(event.node.key);
this.setGroup(key);
}
@action setGroup(data) {

View File

@ -14,3 +14,82 @@ export interface EoExtensionInfo extends ExtensionInfo {
};
[key: string]: any;
}
export enum ContributionPointsPrefix {
category = '@category:',
feature = '@feature:'
}
export const featuresTipsMap = {
importAPI: {
type: 'format',
suggest: '@feature:importAPI'
},
exportAPI: {
type: 'format',
suggest: '@feature:exportAPI'
},
syncAPI: {
type: 'format',
suggest: '@feature:syncAPI'
},
sidebarView: {
type: 'format',
suggest: '@feature:sidebarView'
},
theme: {
type: 'theme',
suggest: '@feature:theme'
}
} as const;
export const categoriesTipsMap = {
'Data Migration': {
type: 'format',
suggest: '@category:Data Migration'
},
Themes: {
type: 'format',
suggest: '@category:Themes'
},
'API Security': {
type: 'format',
suggest: '@category:API Security'
},
Other: {
type: 'format',
suggest: '@category:Other'
}
} as const;
export type FeatureContributionPoints = keyof typeof featuresTipsMap;
export type CategoryContributionPoints = keyof typeof categoriesTipsMap;
export type ContributionPoints = FeatureContributionPoints | CategoryContributionPoints;
export const contributionPoints = Object.keys(featuresTipsMap).concat(Object.keys(categoriesTipsMap));
export const getExtensionCates = () =>
[
{
key: categoriesTipsMap['Data Migration'].suggest,
title: $localize`Data Migration`,
isLeaf: true
},
{
key: categoriesTipsMap.Themes.suggest,
title: $localize`Themes`,
isLeaf: true
},
{
key: categoriesTipsMap['API Security'].suggest,
title: $localize`API Security`,
isLeaf: true
},
{
key: categoriesTipsMap.Other.suggest,
title: $localize`Other`,
isLeaf: true
}
] as const;
export const suggestList = Object.values({ ...categoriesTipsMap, ...featuresTipsMap }).map(n => n.suggest);

View File

@ -1,14 +1,18 @@
import { NgModule } from '@angular/core';
import { EoNgAutoCompleteModule } from 'eo-ng-auto-complete';
import { EoNgSwitchModule } from 'eo-ng-switch';
import { EoNgTabsModule } from 'eo-ng-tabs';
import { EoNgTreeModule } from 'eo-ng-tree';
import { ExtensionDetailComponent } from 'eo/workbench/browser/src/app/pages/extension/detail/extension-detail.component';
import { DownloadCountFormaterPipe } from 'eo/workbench/browser/src/app/pages/extension/download-count-formater.pipe';
import { SharedModule } from 'eo/workbench/browser/src/app/shared/shared.module';
import { NzAvatarModule } from 'ng-zorro-antd/avatar';
import { NzCardModule } from 'ng-zorro-antd/card';
import { NzDescriptionsModule } from 'ng-zorro-antd/descriptions';
import { NzInputNumberModule } from 'ng-zorro-antd/input-number';
import { NzResultModule } from 'ng-zorro-antd/result';
import { NzSpaceModule } from 'ng-zorro-antd/space';
import { NzTagModule } from 'ng-zorro-antd/tag';
// import { ExtensionRoutingModule } from './extension-routing.module';
import { ShadowDomEncapsulationModule } from '../../modules/eo-ui/shadow/shadow-dom-encapsulation.module';
@ -27,8 +31,11 @@ import { ExtensionListComponent } from './list/extension-list.component';
EoNgSwitchModule,
EoNgTreeModule,
NzResultModule,
ShadowDomEncapsulationModule
ShadowDomEncapsulationModule,
NzTagModule,
EoNgAutoCompleteModule,
NzSpaceModule
],
declarations: [ExtensionComponent, ExtensionSettingComponent, ExtensionListComponent, ExtensionDetailComponent]
declarations: [ExtensionComponent, ExtensionSettingComponent, ExtensionListComponent, ExtensionDetailComponent, DownloadCountFormaterPipe]
})
export class ExtensionModule {}

View File

@ -1,20 +1,24 @@
<div class="px-6 overflow-auto extension-list">
<ng-container *ngIf="!renderList?.length">
<ng-container *ngIf="!extensionList?.length">
<nz-spin class="w-fit mx-auto my-[30px]" nzSimple *ngIf="loading"></nz-spin>
<nz-empty class="w-fit mx-auto my-[30px]" *ngIf="!loading"></nz-empty>
</ng-container>
<div class="grid gap-6 py-5 2xl:grid-cols-4 lg:grid-cols-3 sm:grid-cols-1">
<div class="grid gap-6 py-5 2xl:grid-cols-3 lg:grid-cols-3 sm:grid-cols-1">
<div
class="flex flex-col flex-wrap w-full transition-shadow duration-300 rounded border-all plugin-block hover:shadow-lg"
*ngFor="let it of renderList"
*ngFor="let it of extensionList"
(click)="clickExtension($event, it)"
>
<div class="flex w-full p-5">
<div class="flex w-full">
<div class="flex flex-col w-full">
<div class="flex flex-col w-full">
<div class="flex items-center justify-between">
<nz-avatar [nzSrc]="it.logo" nzShape="square"></nz-avatar>
<div class="flex flex-col w-full h-full p-5">
<div class="flex flex-col flex-1 w-full">
<div class="flex items-center">
<nz-avatar class="flex-shrink-0" [nzSize]="35" [nzShape]="'square'" [nzSrc]="it.logo"></nz-avatar>
<div class="flex flex-col flex-1 w-0">
<div class="flex items-center ml-[20px]">
<span class="flex-1 font-bold truncate" [title]="it.title">{{ it.title }}</span>
<div *ngIf="type === 'installed'">
<nz-tag [nzColor]="'default'">v{{ it.version }}</nz-tag>
</div>
<div *ngIf="type !== 'installed'">
<span
*ngIf="extensionService.installedMap?.has(it.name)"
@ -24,27 +28,37 @@
>
</div>
</div>
<div class="flex justify-between text-lg">
<span class="mt-2 mb-1 text-base font-bold">{{ it.title }}</span>
<div *ngIf="type === 'installed'">
<eo-ng-switch
click-stop-propagation
[(ngModel)]="it.enable"
(ngModelChange)="extensionService.toggleEnableExtension(it.name, $event)"
></eo-ng-switch>
</div>
</div>
<span class="text-xs text-tips">{{ it.author }}</span>
<div class="flex mt-[15px] text-[12px] desc leading-[1.65]">{{ it.description }}</div>
</div>
<div class="btn-group" *ngIf="type === 'installed'">
<ng-container *ngIf="it.features?.configuration">
<a eo-ng-button nzType="link" i18n>Setting</a>
<nz-divider nzType="vertical"></nz-divider>
</ng-container>
<a eo-ng-button nzType="link"><span data-id="details" i18n="@@ExtensionDetail">Details</span></a>
<nz-space [nzSplit]="spaceSplit" class="subtitle ml-[20px] text-tips text-[12px]">
<ng-template #spaceSplit>
<nz-divider nzType="vertical"></nz-divider>
</ng-template>
<span *nzSpaceItem>{{ it.author }}</span>
<ng-container *ngIf="type !== 'installed'">
<span class="flex items-center" *nzSpaceItem>
<eo-iconpark-icon class="mr-[5px]" name="download-two" size="14px"></eo-iconpark-icon>
{{ it.downloadCounts | downloadCountFormater }}
</span>
</ng-container>
</nz-space>
</div>
</div>
<div class="flex mt-[15px] text-[12px] desc leading-[1.65] text-tips">{{ it.description }}</div>
</div>
<div class="flex items-center justify-between" *ngIf="type === 'installed'">
<div>
<ng-container *ngIf="it.features?.configuration">
<a eo-ng-button nzType="link" i18n (click)="clickExtension($event, it, 0)">Setting</a>
<nz-divider nzType="vertical"></nz-divider>
</ng-container>
<a eo-ng-button nzType="link" (click)="clickExtension($event, it, it.features?.configuration ? 1 : 0)"
><span data-id="details" i18n="@@ExtensionDetail">Details</span></a
>
</div>
<eo-ng-switch
click-stop-propagation
[(ngModel)]="it.enable"
(ngModelChange)="extensionService.toggleEnableExtension(it.name, $event)"
></eo-ng-switch>
</div>
</div>
</div>

View File

@ -1,12 +1,20 @@
import { Component, OnInit, Output, EventEmitter, Input } from '@angular/core';
import { ElectronService } from 'eo/workbench/browser/src/app/core/services';
import { autorun, computed, observable, makeObservable } from 'mobx';
import { autorun, observable, makeObservable } from 'mobx';
import { ExtensionService } from '../../../shared/services/extensions/extension.service';
import { ExtensionGroupType } from '../extension.model';
const extensionSearch = list => keyword => list.filter(it => it.name.includes(keyword) || it.keywords?.includes(keyword));
import { ContributionPointsPrefix, ExtensionGroupType, suggestList } from '../extension.model';
const extensionSearch = list => {
return (keyword = '') => {
return list.filter(it => {
if (keyword) {
return it.name.includes(keyword) || it.keywords?.includes(keyword);
}
return true;
});
};
};
@Component({
selector: 'eo-extension-list',
templateUrl: './extension-list.component.html',
@ -15,64 +23,74 @@ const extensionSearch = list => keyword => list.filter(it => it.name.includes(ke
export class ExtensionListComponent implements OnInit {
@Input() @observable type: string = ExtensionGroupType.all;
@Input() @observable keyword = '';
@Input() @observable category = '';
@Output() readonly selectChange: EventEmitter<any> = new EventEmitter<any>();
allList = [];
officialList = [];
installedList = [];
extensionList = [];
loading = false;
@computed get renderList() {
if (this.type === 'all') {
return this.allList;
}
if (this.type === 'official') {
return this.officialList;
}
return this.installedList;
}
constructor(public extensionService: ExtensionService, public electron: ElectronService) {}
async ngOnInit() {
makeObservable(this);
autorun(async () => {
switch (this.type) {
case 'all': {
this.allList = [];
this.allList = await this.searchPlugin(this.type, this.keyword);
break;
}
case 'official': {
this.officialList = [];
this.officialList = await this.searchPlugin(this.type, this.keyword);
}
default: {
this.installedList = [];
this.installedList = await this.searchPlugin(this.type, this.keyword);
break;
}
if (this.keyword) {
const notCompleteSuggest = suggestList.some(n => n.startsWith(this.keyword) && this.keyword !== n);
if (notCompleteSuggest) return;
}
let type = this.type;
if (type.startsWith(ContributionPointsPrefix.category)) {
type = 'category';
this.category = this.type.slice(ContributionPointsPrefix.category.length);
}
this.extensionList = [];
this.extensionList = await this.searchPlugin(type, { keyword: this.keyword, category: this.category });
});
}
clickExtension(event, item) {
clickExtension(event: MouseEvent, item, nzSelectedIndex?) {
event.stopPropagation();
item.nzSelectedIndex = nzSelectedIndex;
this.selectChange.emit(item);
}
async searchPlugin(groupType, keyword = '') {
async searchPlugin(groupType, { keyword = '', category = '', feature = '' }) {
this.loading = true;
const suggest = suggestList.find(n => keyword.startsWith(n));
if (suggest) {
const prefix = Object.values(ContributionPointsPrefix).find(n => keyword.startsWith(n));
const text = suggest.slice(prefix.length);
keyword = keyword.slice(suggest.length).trim();
if (prefix === ContributionPointsPrefix.feature) {
groupType = 'feature';
feature = text;
} else if (prefix === ContributionPointsPrefix.category) {
groupType = 'category';
category = text;
}
}
const func = {
installed: () => {
const list = this.extensionService.getInstalledList();
return extensionSearch(list)(keyword);
},
official: async () => {
const authorName = ['Postcat'];
const { data }: any = await this.extensionService.requestList();
return extensionSearch(data.filter(it => authorName.includes(it.author)))(keyword);
const [{ data }]: any = await this.extensionService.requestList('list', { author: 'Postcat', keyword });
return data;
},
all: async () => {
const { data }: any = await this.extensionService.requestList();
return extensionSearch(data)(keyword);
const [{ data }]: any = await this.extensionService.requestList('list', { keyword });
return data;
},
category: async () => {
const [{ data }]: any = await this.extensionService.requestList('list', { category, keyword });
return data;
},
feature: async () => {
const [{ data }]: any = await this.extensionService.requestList('list', { feature, keyword });
return data;
}
};
try {
return await func[groupType]();
const result = await func[groupType]();
return result;
} catch (error) {
} finally {
this.loading = false;

View File

@ -47,6 +47,7 @@ export interface ExtensionInfo {
//Entry js file,node environment
node: string;
title: string;
downloadCounts: number;
// extension logo
logo: string;
//Contribution Feature

View File

@ -1,4 +1,4 @@
import { HttpClient } from '@angular/common/http';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { ElectronService } from 'eo/workbench/browser/src/app/core/services';
import { LanguageService } from 'eo/workbench/browser/src/app/core/services/language/language.service';
@ -6,7 +6,7 @@ import { DISABLE_EXTENSION_NAMES } from 'eo/workbench/browser/src/app/shared/con
import { FeatureInfo, ExtensionInfo, SidebarView } from 'eo/workbench/browser/src/app/shared/models/extension-manager';
import { MessageService } from 'eo/workbench/browser/src/app/shared/services/message';
import { APP_CONFIG } from 'eo/workbench/browser/src/environments/environment';
import { lastValueFrom } from 'rxjs';
import { lastValueFrom, Subscription } from 'rxjs';
import { ExtensionStoreService } from './extension-store.service';
import { WebExtensionService } from './webExtension.service';
@ -21,7 +21,8 @@ export class ExtensionService {
extensionIDs: string[] = [];
HOST = APP_CONFIG.EXTENSION_URL;
installedList: ExtensionInfo[] = [];
installedMap: Map<string, ExtensionInfo>;
installedMap: Map<string, ExtensionInfo> = new Map();
private requestPending: Subscription | null = null;
constructor(
private http: HttpClient,
private electron: ElectronService,
@ -92,44 +93,65 @@ export class ExtensionService {
isInstalled(name) {
return this.installedList.includes(name);
}
public async requestList(type = 'list') {
const result: any = await lastValueFrom(this.http.get(`${this.HOST}/list?locale=${this.language.systemLanguage}`), {
defaultValue: []
});
const debugExtensions = [];
public async requestList(type = 'list', queryParams = {}) {
this.requestPending?.unsubscribe();
return new Promise((resolve, reject) => {
const params = JSON.parse(JSON.stringify({ locale: this.language.systemLanguage, ...queryParams }));
if (type !== 'init') {
for (let i = 0; i < this.webExtensionService.debugExtensionNames.length; i++) {
const name = this.webExtensionService.debugExtensionNames[i];
const hasExist = this.installedList.some(val => val.name === name);
if (hasExist) continue;
debugExtensions.push(await this.webExtensionService.getDebugExtensionsPkgInfo(name));
}
}
this.requestPending = this.http.get<any>(`${this.HOST}/list`, { params }).subscribe({
next: async result => {
const debugExtensions = [];
const originData = structuredClone(result.data);
result.data = [
...result.data.filter(val => this.installedList.every(childVal => childVal.name !== val.name)),
//Local debug package
...this.installedList.map(module => {
const extension = result.data.find(it => it.name === module.name);
if (extension) {
module.i18n = extension.i18n;
if (type !== 'init') {
for (let i = 0; i < this.webExtensionService.debugExtensionNames.length; i++) {
const name = this.webExtensionService.debugExtensionNames[i];
const hasExist = this.installedList.some(val => val.name === name);
if (hasExist) continue;
debugExtensions.push(await this.webExtensionService.getDebugExtensionsPkgInfo(name));
}
}
result.data = [
...result.data.filter(val => this.installedList.every(childVal => childVal.name !== val.name)),
//Local debug package
...this.installedList
.filter(n => {
const target = result.data.find(m => n.name === m.name);
n.downloadCounts = target?.downloadCounts;
return target;
})
.map(module => {
const extension = result.data.find(it => it.name === module.name);
if (extension) {
module.i18n = extension.i18n;
}
return module;
}),
...debugExtensions
];
//Handle featue data
result.data = result.data.map(module => {
let result = this.parseExtensionInfo(module);
return result;
});
//Get debug extensions
this.webExtensionService.debugExtensions = result.data.filter(val => val.isDebug);
this.store.setExtensionList(result.data);
this.requestPending = null;
resolve([result, originData]);
},
error: () => {
this.requestPending = null;
reject([]);
}
return module;
}),
...debugExtensions
];
//Handle featue data
result.data = result.data.map(module => {
let result = this.parseExtensionInfo(module);
return result;
});
});
//Get debug extensions
this.webExtensionService.debugExtensions = result.data.filter(val => val.isDebug);
this.store.setExtensionList(result.data);
return result;
// const result: any = await lastValueFrom(, {
// defaultValue: []
// });
}
async getDetail(name): Promise<any> {
let result = {} as ExtensionInfo;

File diff suppressed because one or more lines are too long

View File

@ -45,17 +45,22 @@ const socket = (port = _post) => {
const link = /^(wss:\/{2})|(ws:\/{2})\S+$/m.test(request.uri.trim())
? request.uri.trim()
: request.protocol + '://' + request.uri.trim().replace('//', '');
ws = new WebSocket(link, {
headers: request?.requestParams.headerParams
?.filter(it => it.name && it.value)
.reduce(
(total, { name, value }) => ({
...total,
[name]: value
}),
{}
)
});
try {
ws = new WebSocket(link, {
headers: request?.requestParams.headerParams
?.filter(it => it.name && it.value)
.reduce(
(total, { name, value }) => ({
...total,
[name]: value
}),
{}
)
});
} catch (error) {
socket.emit('ws-client', { type: 'ws-connect-back', status: -1, content: error });
}
ws.on('error', err => {
socket.emit('ws-client', { type: 'ws-connect-back', status: -1, content: err });
unlisten();

509
yarn.lock

File diff suppressed because it is too large Load Diff