Feat/v0.5.0 - API Case (#279)

* fix(api-test): Import request headers could not be sent

* fix: notification alway show in safari

* types: add comment

* build: update iconpark

* refactor: use decoration for shortcut

* feat: delete useless code

* wip: afterTabActivated

* refactor: tab logic

* refactor: remove initial model logic

* refactor: api edit remove inittimes

* refactor: tab execute async request

* refactor: edit api

* refactor: rename all EoNgFeedbackMessageService to feedback

* refactor: remove collection type logic

* refactor: add type inherited

* feat: remove tab when delete group resource

* wip: delete useless

* chore: deplywindows config

* feat: mock and new people guide develop

* fix(import-api): schema form

* refactor: api dto

* feat: add group mock

* fix: storage tab content error

* feat: mock & markdown & steps & action develop

* feat: tabs route

* wip: remove useless

* feat: mock & markdown & steps & action develop

* feat: mock & markdown & steps & action develop

* feat: markdown-loader

* feat: mock develop

* fix: mock dir error

* feat: mock develop

* fix: tab refresh error

* feat: mock develop

* feat: mock joint debugging

* feat: change initial api data

* fix: get id error

* feat: mock develop

* feat: add mock url at local indexeddb

* fix: isForm chagne detect api page error

* feat: mock develop

* refactor: all api operate

* fix: delete tab not close tab

* feat: mock develop

* feat: guide develop

* feat: add case

* feat: mock develop and action develop

* feat: mock develop and action develop

* feat: detail/delete

* feat: group and case fit

* feat(api-case): save name and save case

* feat: translate

* wip: delete useless

* feat: mock develop and action develop

* fix: test page height

* feat: mock develop and guide develop

* feat: mock develop and guide develop

* refactor(mock): delete useless code

* fix: new api error

* chore: change build

* feat: mock develop and guide develop

* fix: delete case error

* fix: local case some problem

* test(e2e): fixed some case

* fix: restore from test page lack of params

* fix: resolve bug

* fix: resolve bug

* fix: resolve bug

* feat: change dynamic mock to system mock

* fix: delete case no tips

* fix: resolve bug

* feat: add local test tips

* fix: resolve bug

* fix: case remote error

* fix: resolve bug

* fix: resolve bug

* fix: group tree shrink problem

* fix: add case error

* fix: resolve bug

* fix: local case edit error

* wip: close update log

* test: add e2e

* fix(new-pie): lack of image

* feat: mock and case can't sort

* test(e2e): test api& edit api

* refactor: remove param.example logic

* feat: translate about

* feat: import as curl

* feat: add scriptList at edit page

* feat: translate

* fix: open case error

* fix: import postcat error

* fix: case name lead to page overflow

* fix: offline can't install extension

* fix: share document read case

---------

Co-authored-by: sunzhouyang <sunzhouyang@eolink.com>
This commit is contained in:
Scarqin 2023-04-03 19:34:24 +08:00 committed by GitHub
parent fa99f30a25
commit 859c61dddc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
288 changed files with 28092 additions and 12608 deletions

View File

@ -26,7 +26,7 @@ jobs:
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)

View File

@ -144,9 +144,11 @@ yarn add @angular/cli --global
|命令 |描述 |
| ------------ | ------------ |
|yarn start |开发模式下,同时运行在浏览器和桌面端 |
|yarn start:zh|中文开发模式,同时运行在浏览器和桌面端|
|yarn start:web |仅运行在浏览器,同时开启后端代理 |
|yarn start:electron|仅运行在桌面端 |
> 本项目 i18n 使用的是编译手段,所以开发时无法切换语言
### 打包构建
|命令 |描述 |

View File

@ -1,4 +1,4 @@
const baseUrl = './src/browser/src/app/shared/services/storage/';
const baseUrl = './src/browser/src/app/services/storage/';
module.exports = {
entry: {
// target: ["./test/apiData.ts", "./test/env.ts"]

14771
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "postcat",
"version": "0.4.2",
"version": "0.5.0",
"main": "out/app/electron-main/main.js",
"description": "A lightweight, extensible API tool",
"homepage": "https://github.com/Postcatlab/postcat.git",
@ -17,13 +17,14 @@
"start:web": "yarn workspace postcat-web run start",
"start:web:zh": "yarn workspace postcat-web run start:zh",
"start:electron": "npm-run-all -p web:start:direct electron:dev",
"build": "npx patch-package && npm-run-all -s electron:build:web clear:electron:tsc electron:tsc && npx esno scripts/build.ts",
"build": "npx patch-package && npm-run-all -s electron:build:browser clear:electron:tsc electron:tsc && npx esno scripts/build.ts",
"build:web": "yarn workspace postcat-web run build:web",
"build:browser": "yarn workspace postcat-web run build",
"build:static": "npm run clear:electron:tsc&&npm run electron:tsc && npx esno scripts/build.ts",
"build:win:noSign": "npm run clear:electron:tsc&&npm run electron:tsc && npx esno scripts/buildNoSign.ts",
"electron:build:web": "yarn workspace postcat-web run build",
"electron:build:browser": "yarn workspace postcat-web run build",
"electron:static": "npm run electron:tsc && electron .",
"release": "npm-run-all -s electron:build:web electron:tsc && npx esno scripts/build.ts --publish=always && node scripts/upload.js",
"release": "npm-run-all -s electron:build:browser electron:tsc && npx esno scripts/build.ts --publish=always && node scripts/upload.js",
"test": "npm-run-all --serial test:*",
"e2e": "cd test/e2e&&npx playwright test -c playwright.config.ts",
"test:e2e": "yarn e2e",
@ -52,6 +53,7 @@
"electron-updater": "^5.3.0",
"express": "4.18.1",
"fix-path": "3.0.0",
"html-loader": "4.2.0",
"http-server": "14.1.1",
"iconv-lite": "^0.6.3",
"jquery": "3.6.1",
@ -61,17 +63,19 @@
"npm": "6.14.17",
"pm2": "5.2.2",
"portfinder": "1.0.32",
"postman-sandbox": "^4.2.3",
"qiniu": "^6.0.0",
"resolve": "^1.22.1",
"showdown": "2.1.0",
"socket.io": "4.5.4",
"ws": "8.12.0",
"xml2js": "0.4.23",
"yaml": "2.2.1",
"postman-sandbox": "^4.2.3"
"yaml": "2.2.1"
},
"devDependencies": {
"@commitlint/cli": "~17.3.0",
"@commitlint/config-conventional": "~17.3.0",
"@playwright/test": "1.32.1",
"@types/node": "18.0.0",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
@ -133,4 +137,4 @@
"prettier --write"
]
}
}
}

View File

@ -52,8 +52,7 @@ function hashFile(file: string, algorithm = 'sha512', encoding: 'base64' | 'hex'
.pipe(hash, { end: false });
});
}
const config: Configuration = {
export const ELECTRON_BUILD_CONFIG: Configuration = {
appId: '.postcat.io',
productName: 'Postcat',
asar: true,
@ -95,6 +94,34 @@ const config: Configuration = {
schemes: ['eoapi']
}
],
portable: {
splashImage: 'src/app/common/images/postcat.bmp'
},
dmg: {
sign: false
},
afterSign: 'scripts/notarize.js',
linux: {
icon: 'src/app/common/images/',
target: ['AppImage']
},
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: [
{
target: 'default',
arch: ['x64', 'arm64']
}
]
}
};
const config: Configuration = {
...ELECTRON_BUILD_CONFIG,
win: {
icon: 'src/app/common/images/logo.ico',
verifyUpdateCodeSignature: false,
@ -108,32 +135,6 @@ const config: 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']
target: [
{
target: 'default',
arch: ['x64', 'arm64']
}
]
},
dmg: {
sign: false
},
afterSign: 'scripts/notarize.js',
linux: {
icon: 'src/app/common/images/',
target: ['AppImage']
}
};

View File

@ -5,11 +5,10 @@ import minimist from 'minimist';
import YAML from 'yaml';
import pkgInfo from '../package.json';
import { ELETRON_APP_CONFIG } from '../src/environment';
import { ELECTRON_BUILD_CONFIG } from './build';
import { execSync, exec, spawn } from 'node:child_process';
import { createHash } from 'node:crypto';
import { copyFileSync, createReadStream, readFileSync, writeFileSync } from 'node:fs';
import { exec, spawn } from 'node:child_process';
import { writeFileSync } from 'node:fs';
import path, { resolve } from 'node:path';
import { exit, platform } from 'node:process';
@ -39,81 +38,10 @@ if (process.platform === 'win32') {
}
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/browser/dist/**/*',
'out/browser/src/**/*.js*',
'out/node/test-server/**/*.js*',
'out/app/common/**/*',
'!**/*.ts'
],
publish: [
'github',
{
provider: 'generic',
url: ELETRON_APP_CONFIG.BASE_DOWNLOAD_URL
}
],
generateUpdatesFilesForAllChannels: true,
nsis: {
guid: 'Postcat',
oneClick: false,
allowElevation: true,
allowToChangeInstallationDirectory: true,
// for win - 将协议写入主机的脚本
include: 'scripts/urlProtoco.nsh'
},
protocols: [
// for macOS - 用于在主机注册指定协议
{
name: 'eoapi',
schemes: ['eoapi']
}
],
...ELECTRON_BUILD_CONFIG,
win: {
icon: 'src/app/common/images/logo.ico',
target: ['nsis', 'portable']
// extraFiles: [
// {
// from: './build/Uninstall Postcat.exe',
// to: '.'
// }
// ]
},
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']
target: [
{
target: 'default',
arch: ['x64', 'arm64']
}
]
},
dmg: {
sign: false
},
afterSign: 'scripts/notarize.js',
linux: {
icon: 'src/app/common/images/',
target: ['AppImage']
}
};

View File

@ -1,6 +1,8 @@
const { Client } = require('ssh2');
const conn = new Client();
const originNodeVersion = '12.22.10';
const nodeVersion = '16.19.1';
conn
.on('ready', () => {
console.log('Client :: ready');
@ -28,8 +30,8 @@ conn
'git reset --hard',
'git checkout build/windows',
...Array.from({ length: 5 }).map(_ => 'git pull'),
'nvm install 16.19.1',
'nvm use 16.19.1',
`nvm install ${nodeVersion}`,
`nvm use ${nodeVersion}`,
`
cat>./scripts/qiniu_env.js<<EOF
${process.env.QINIU_ENV_JS}
@ -37,7 +39,7 @@ EOF
`,
'yarn install',
'yarn release',
'nvm use 12.22.10',
`nvm use ${originNodeVersion}`,
'echo Windows 打包发布完成!'
].join('\r\n')
);

View File

@ -4,7 +4,6 @@ import Store from 'electron-store';
import { LanguageService } from 'pc/app/electron-main/language.service';
import { MockServer } from 'pc/platform/node/mock-server';
import {
GET_EXT_TABS,
GET_FEATURE,
GET_MOCK_URL,
GET_MODULE,
@ -229,8 +228,6 @@ try {
const getWebsocketPort = () => Promise.resolve(websocketPort);
const getExtTabs = arg => Promise.resolve(moduleManager.getExtTabs(arg.data.extName));
const loginWith = arg => {
if (loginWindow) {
loginWindow.destroy();
@ -278,7 +275,6 @@ try {
[GET_FEATURE]: getFeature,
[GET_MOCK_URL]: getMockUrl,
[GET_WEBSOCKET_PORT]: getWebsocketPort,
[GET_EXT_TABS]: getExtTabs,
// * It is eletron, open a new window for login
[LOGIN_WITH]: loginWith,
[GET_SIDEBAR_VIEW]: getSidebarView,

View File

@ -7,6 +7,7 @@ dist/
/app-builds
/release
src/**/*.js
!markdown-loader.js
!scripts/*.js
!src/karma.conf.js
!src/assets/font/*.js

View File

@ -119,7 +119,7 @@
},
"defaultProject": "postcat",
"schematics": {
"@schematics/angular:component": { "prefix": "pc", "style": "scss", "inlineStyle": true, "inlineTemplate": true },
"@schematics/angular:component": { "prefix": "pc", "style": "scss", "inlineStyle": false, "inlineTemplate": true },
"@schematics/angular:directive": { "prefix": "pc" }
}
}

View File

@ -50,12 +50,19 @@ module.exports = (config, options) => {
type: 'asset/resource',
resourceQuery: { not: [/\?ngResource/] }
},
{
// .md结尾的文件使用markdown-loader规则
test: /\.md$/,
use: ['html-loader', './markdown-loader']
},
...config.module.rules
];
config.experiments = {
topLevelAwait: true
};
Object.assign(config, {
experiments: {
topLevelAwait: true
}
});
// console.log('config', config.module.rules);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
const markdownIt = require('markdown-it');
module.exports = source => {
let md = new markdownIt();
const html = md.render(source);
return html;
};

View File

@ -38,6 +38,7 @@
"@xmagic/ngx-wujie": "1.0.0-rc.20",
"ajv": "8.12.0",
"color": "^4.2.3",
"compare-versions": "6.0.0-rc.1",
"core-js": "3.27.2",
"eo-ng-auto-complete": "0.1.11",
"eo-ng-button": "0.1.10",
@ -62,8 +63,10 @@
"monaco-editor": "0.33.0",
"ng-zorro-antd": "15.0.3",
"omit-deep-lodash": "1.1.7",
"parse-multipart-data": "1.5.0",
"qs": "6.11.0",
"rxjs": "7.8.0",
"shellwords-ts": "3.0.1",
"socket.io-client": "4.5.4",
"tslib": "^2.5.0",
"wujie": "1.0.6",
@ -82,6 +85,7 @@
"@angular/localize": "15.1.3",
"@types/color": "3.0.3",
"@types/jasmine": "4.3.1",
"@types/parse-multipart": "1.0.0",
"@types/jasminewd2": "2.0.10",
"@types/lodash-es": "4.17.6",
"@types/markdown-it": "12.2.3",

View File

@ -15,8 +15,6 @@ import { LanguageService } from 'pc/browser/src/app/core/services/language/langu
import { NotificationService } from 'pc/browser/src/app/core/services/notification.service';
import { ExtensionService } from 'pc/browser/src/app/services/extensions/extension.service';
import { GlobalProvider } from 'pc/browser/src/app/services/globalProvider';
import { IndexedDBStorage } from 'pc/browser/src/app/services/storage/IndexedDB/lib';
import { HttpStorage } from 'pc/browser/src/app/services/storage/http/lib';
import { BaseUrlInterceptor } from 'pc/browser/src/app/services/storage/http/lib/baseUrl.service';
import { APP_CONFIG } from 'pc/browser/src/environments/environment';
@ -46,8 +44,6 @@ registerLocaleData(zh);
providers: [
MockService,
ExtensionService,
IndexedDBStorage,
HttpStorage,
ThemeService,
{ provide: ErrorHandler, useClass: GlobalErrorHandler },
{

View File

@ -1,9 +1,16 @@
import { CommonModule } from '@angular/common';
import { Component, Input, OnInit } from '@angular/core';
import { EoNgButtonModule } from 'eo-ng-button';
import { EoNgDropdownModule } from 'eo-ng-dropdown';
import { SharedModule } from 'pc/browser/src/app/shared/shared.module';
import { ElectronService, WebService } from '../../core/services';
import { EoIconparkIconModule } from '../eo-ui/iconpark-icon/eo-iconpark-icon.module';
@Component({
selector: 'pc-download-client',
standalone: true,
imports: [CommonModule, EoIconparkIconModule, SharedModule, EoNgButtonModule, EoNgDropdownModule],
template: `<ng-container *ngIf="!electron.isElectron">
<button *ngIf="btnType === 'icon'" eo-ng-button nzType="text" eo-ng-dropdown [nzDropdownMenu]="download">
<eo-iconpark-icon name="download" size="14px"></eo-iconpark-icon>

View File

@ -1,15 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { EoNgButtonModule } from 'eo-ng-button';
import { EoNgDropdownModule } from 'eo-ng-dropdown';
import { SharedModule } from 'pc/browser/src/app/shared/shared.module';
import { EoIconparkIconModule } from '../eo-ui/iconpark-icon/eo-iconpark-icon.module';
import { DownloadClientComponent } from './download-client.component';
@NgModule({
imports: [CommonModule, EoIconparkIconModule, SharedModule, EoNgButtonModule, EoNgDropdownModule],
declarations: [DownloadClientComponent],
exports: [DownloadClientComponent]
})
export class DownloadClientModule {}

View File

@ -112,7 +112,7 @@ export class EoMonacoEditorComponent implements AfterViewInit, OnInit, OnChanges
}
constructor(
private message: EoNgFeedbackMessageService,
private feedback: EoNgFeedbackMessageService,
private electron: ElectronService,
private theme: ThemeService,
elementRef: ElementRef
@ -299,7 +299,7 @@ export class EoMonacoEditorComponent implements AfterViewInit, OnInit, OnChanges
const value = this.codeEdtor.getValue();
if (navigator.clipboard) {
navigator.clipboard.writeText(value);
this.message.success($localize`Copied`);
this.feedback.success($localize`Copied`);
return;
}
break;

View File

@ -39,7 +39,7 @@ export class TabOperateService {
private tabStorage: TabStorageService,
private messageService: MessageService,
private router: Router,
private message: EoNgFeedbackMessageService
private feedback: EoNgFeedbackMessageService
) {}
//Init tab info
//Maybe from tab cache info or router url
@ -50,7 +50,6 @@ export class TabOperateService {
: this.tabStorage.getPersistenceStorage({
handleDataBeforeGetCache: inArg.handleDataBeforeGetCache
});
//parse result for router change
const tabCache = this.filterValidTab(tabStorage);
const validTabItem = this.generateTabFromUrl(this.router.url);
@ -140,10 +139,14 @@ export class TabOperateService {
* */
batchClose(ids) {
const tabOrder = this.tabStorage.tabOrder.filter(uuid => !ids.includes(uuid));
this.tabStorage.resetTabsByOrdr(tabOrder);
this.tabStorage.resetTabsByOrder(tabOrder);
if (this.tabStorage.tabOrder.length === 0) {
this.newDefaultTab();
return;
}
//Update childView
this.navigateByTab(this.getCurrentTab());
}
/**
@ -155,8 +158,11 @@ export class TabOperateService {
if (!tab) {
return;
}
const queryParams = { pageID: tab.params?.pageID, ...tab.params };
if (!queryParams.pageID) Reflect.deleteProperty(queryParams, 'pageID');
const queryParams = { ...tab.params };
//Reset params ID
if (tab.params?.pageID) {
queryParams.pageID = tab.params.pageID;
}
this.router.navigate([tab.pathname], {
queryParams
});
@ -168,31 +174,29 @@ export class TabOperateService {
*
* @param tab
*/
getSameTab(
tab: Partial<TabItem>,
opts: {
match: 'all' | 'uuid';
} = { match: 'all' }
): TabItem | null {
getSameTab(tab: Partial<TabItem>): TabItem | null {
let result = null;
if (!tab.params.uuid) {
const sameTabIDTab = this.tabStorage.tabsByID.get(tab.uuid);
if (sameTabIDTab && sameTabIDTab.pathname === tab.pathname) {
return sameTabIDTab;
}
return result;
}
//Get exist params.uuid content tab,same pathname and uuid match
const mapObj = Object.fromEntries(this.tabStorage.tabsByID);
for (const key in mapObj) {
if (Object.prototype.hasOwnProperty.call(mapObj, key)) {
const tabInfo = mapObj[key];
if (tabInfo.pathname !== tab.pathname && opts.match === 'all') continue;
if (tabInfo.params.uuid === tab.params.uuid) {
result = tabInfo;
break;
}
}
//Uuid match first
// if (tab.params.uuid) {
// //Get exist params.uuid content tab,same pathname and uuid match
// const mapObj = Object.fromEntries(this.tabStorage.tabsByID);
// for (const key in mapObj) {
// if (Object.prototype.hasOwnProperty.call(mapObj, key)) {
// const tabInfo = mapObj[key];
// if (tabInfo.pathname !== tab.pathname) continue;
// if (tabInfo.params.uuid === tab.params.uuid) {
// result = tabInfo;
// break;
// }
// }
// }
// return result;
// }
//PageID match second
const sameTabIDTab = this.tabStorage.tabsByID.get(tab.uuid);
if (sameTabIDTab && sameTabIDTab.pathname === tab.pathname) {
return sameTabIDTab;
}
return result;
}
@ -280,10 +284,14 @@ export class TabOperateService {
*/
if (existTab) {
this.selectedIndex = this.tabStorage.tabOrder.findIndex(uuid => uuid === existTab.uuid);
//* Update tab info,maybe params changed
//!Get newest tab content,If the initialization is too fast, the baseContent content will be overwritten here
const newestData = this.getSameTab(routeTab);
//Reload childView when reselected it
this.updateChildView();
//* Update tab info,maybe params changed
this.tabStorage.tabsByID.set(existTab.uuid, { ...existTab, params: { ...existTab.params, ...nextTab.params } });
this.tabStorage.setTabByID({ ...newestData, params: { ...newestData.params, ...nextTab.params } });
return;
}
//!Same params.uuid can only open one Tab
@ -360,10 +368,10 @@ export class TabOperateService {
break;
}
}
this.tabStorage.resetTabsByOrdr(tabsObj.left);
this.tabStorage.resetTabsByOrder(tabsObj.left);
this.selectedIndex = tabsObj.selectedIndex;
if (tabsObj.needTips) {
this.message.warning($localize`Program will not close unsaved tabs`);
this.feedback.warning($localize`Program will not close unsaved tabs`);
}
}
/**
@ -377,16 +385,16 @@ export class TabOperateService {
for (const key in mapObj) {
if (Object.prototype.hasOwnProperty.call(mapObj, key)) {
const tab = mapObj[key];
if (tab.params.uuid && tab.params.uuid === inTab.params.uuid) {
const mergeTab = this.preventBlankTab(tab, inTab);
mergeTab.content = tab.content;
mergeTab.baseContent = tab.baseContent;
mergeTab.extends = Object.assign(mergeTab.extends || {}, tab.extends);
this.selectedIndex = this.tabStorage.tabOrder.findIndex(uuid => uuid === tab.uuid);
this.tabStorage.updateTab(this.selectedIndex, mergeTab);
this.updateChildView();
return true;
}
if (tab.params?.uuid !== inTab.params.uuid) continue;
const mergeTab = this.preventBlankTab(tab, inTab);
mergeTab.content = tab.content;
mergeTab.baseContent = tab.baseContent;
mergeTab.extends = Object.assign(mergeTab.extends || {}, tab.extends);
this.selectedIndex = this.tabStorage.tabOrder.findIndex(uuid => uuid === tab.uuid);
this.tabStorage.updateTab(this.selectedIndex, mergeTab);
this.updateChildView();
return true;
}
}
return false;
@ -409,7 +417,7 @@ export class TabOperateService {
if (keepID && uuid === keepID) {
return true;
}
if (this.tabStorage.tabsByID.get(uuid).hasChanged) {
if (this.tabStorage.tabsByID.get(uuid)?.hasChanged) {
tabsObj.needTips = true;
return true;
}
@ -445,7 +453,7 @@ export class TabOperateService {
return result;
}
private updateChildView() {
this.messageService.send({ type: 'tabContentInit', data: {} });
this.messageService.send({ type: 'tabContentInit', data: { uuid: this.getCurrentTab()?.uuid } });
}
/**
* Get valid tab item
@ -463,9 +471,9 @@ export class TabOperateService {
if (!tabItem) {
return false;
}
const validTab = this.BASIC_TABS.find(val => val.id === tabItem.id);
const validTab = this.BASIC_TABS.find(val => val.uniqueName === tabItem.uniqueName);
if (!validTab) {
delete cache.tabsByID[id];
Reflect.deleteProperty(cache.tabsByID, id);
} else {
tabItem.pathname = validTab.pathname;
}

View File

@ -19,18 +19,21 @@ export class TabStorageService {
this.tabStorageKey = inArg.tabStorageKey;
}
addTab(tabItem) {
if (this.tabsByID.has(tabItem.uuid)) {
if (this.tabsByID.has(tabItem.uuid) && this.tabOrder.some(uuid => uuid === tabItem.uuid)) {
throw new Error(`EO_ERROR: can't add same id tab`);
}
this.tabOrder.push(tabItem.uuid);
this.tabsByID.set(tabItem.uuid, tabItem);
this.setTabByID(tabItem);
}
updateTab(index, tabItem) {
this.tabsByID.delete(this.tabOrder[index]);
this.tabOrder[index] = tabItem.uuid;
this.setTabByID(tabItem);
}
setTabByID(tabItem) {
this.tabsByID.set(tabItem.uuid, tabItem);
}
resetTabsByOrdr(order) {
resetTabsByOrder(order) {
const tabs = new Map();
this.tabsByID.forEach((value, key) => {
if (!order.includes(key)) {
@ -55,7 +58,8 @@ export class TabStorageService {
setPersistenceStorage(selectedIndex, opts) {
let tabsByID = Object.fromEntries(this.tabsByID);
Object.values(tabsByID).forEach(val => {
if (val.type === 'preview') {
//Remove cache when no change
if (val.type === 'preview' || (val.type === 'edit' && !val.hasChanged)) {
['baseContent', 'content'].forEach(keyName => {
val[keyName] = null;
});

View File

@ -1,4 +1,3 @@
<!-- {{ getConsoleTabs() | json }} -->
<eo-ng-tabset
[(nzSelectedIndex)]="tabOperate.selectedIndex"
nzType="editable-card"
@ -89,4 +88,5 @@
</ul>
</eo-ng-dropdown-menu>
</div>
<!-- {{ getConsoleTabs() }} -->
</ng-template>

View File

@ -7,7 +7,7 @@ import { TabOperateService } from 'pc/browser/src/app/components/eo-ui/tab/tab-o
import { TabStorageService } from 'pc/browser/src/app/components/eo-ui/tab/tab-storage.service';
import { TabItem, TabOperate } from 'pc/browser/src/app/components/eo-ui/tab/tab.model';
import { TraceService } from 'pc/browser/src/app/services/trace.service';
import { StoreService } from 'pc/browser/src/app/store/state.service';
import { StoreService } from 'pc/browser/src/app/shared/store/state.service';
import { filter, Subscription } from 'rxjs';
import { ModalService } from '../../../services/modal.service';
@ -85,7 +85,7 @@ export class EoTabComponent implements OnInit, OnDestroy {
return;
}
$event.stopPropagation();
if (!tab.hasChanged) {
if (!tab?.hasChanged) {
this.tabOperate.closeTab(index);
return;
}
@ -132,6 +132,7 @@ export class EoTabComponent implements OnInit, OnDestroy {
return;
}
tabs.push({
baseContent: tab.baseContent,
uuid: tab.uuid,
type: tab.type,
title: tab.title,
@ -139,7 +140,6 @@ export class EoTabComponent implements OnInit, OnDestroy {
params: tab.params
});
});
console.log(tabs);
return tabs;
}
getTabs() {
@ -148,17 +148,22 @@ export class EoTabComponent implements OnInit, OnDestroy {
return tabs;
}
/**
* Get tab by url with same content
* Get tab by tab id
*
* @param url
* @param uuid
*/
getTabByID(uuid: TabItem['uuid']) {
return this.tabStorage.tabsByID.get(uuid);
}
/**
* Get Tab id by child component resource id
*
* @param uuid queryparams uuid
* @returns
*/
getExistTabByUrl(url: string): TabItem | null {
const existTab = this.tabOperate.getSameTab(this.tabOperate.getBasicInfoFromUrl(url));
if (!existTab) {
return null;
}
return existTab;
getTabByParamsID(uuid: TabItem['params']['uuid']) {
const tabID = this.tabStorage.tabOrder.find(tabID => this.tabStorage.tabsByID.get(tabID)?.params?.uuid === uuid);
return this.tabStorage.tabsByID.get(tabID);
}
getCurrentTab() {
return this.tabOperate.getCurrentTab();
@ -169,14 +174,14 @@ export class EoTabComponent implements OnInit, OnDestroy {
/**
* update tab
*
* @param url when url exist in tabs,replace
* @param uuid tab uuid
* @param tabItem
* @returns
*/
updatePartialTab(url: string, tabItem: Partial<TabItem>) {
const existTab = this.getExistTabByUrl(url);
updatePartialTab(uuid: string | number, tabItem: Partial<TabItem>) {
const existTab = this.getTabByID(uuid);
if (!existTab) {
pcConsole.error(`:updatePartialTab fail,can't find exist tab to fixed url:${url}`);
pcConsole.error(`:updatePartialTab fail,can't find exist tab to fixed uuid:${uuid}`);
return;
}
const index = this.tabStorage.tabOrder.findIndex(uuid => uuid === existTab.uuid);
@ -185,6 +190,7 @@ export class EoTabComponent implements OnInit, OnDestroy {
...tabItem,
extends: { ...existTab.extends, ...tabItem.extends }
});
// console.log('updatePartialTabSuccess', this.tabStorage.tabsByID.get(uuid));
}
/**
* Cache tab header/tabs content for restore when page close or component destroy

View File

@ -1,51 +1,62 @@
import type { EventEmitter } from '@angular/core';
import { PageUniqueName } from 'pc/browser/src/app/pages/workspace/project/api/api-tab.service';
export enum TabOperate {
closeOther = 'closeOther',
closeAll = 'closeAll',
closeLeft = 'closeLeft',
closeRight = 'closeRight'
closeRight = 'closeRight',
forceCloseAll = 'forceCloseAll',
forceCloseOther = 'forceCloseOther'
}
export type storageTab = {
selectedIndex: number;
tabOrder: number[];
tabsByID: { [key: number]: TabItem };
};
export declare interface TabViewComponent {
/**
* View Component model
* Usually restored model from tab cache
*/
model?: any;
/**
* Initial model for check form is change
*/
initialModel?: any;
declare interface TabViewComponent {
/**
* Emit view component data has init event for initial tab title data/loading..
*/
eoOnInit: EventEmitter<any>;
/**
* Emit view component data has been saved
* Check the page can leave,if false will not switch tab
*/
afterSaved?: EventEmitter<any>;
checkTabCanLeave?(closeTarget: TabItem): Promise<boolean>;
beforeTabClose?(): Promise<any>;
}
export declare interface PreviewTabViewComponent extends TabViewComponent {}
export declare interface EditTabViewComponent extends TabViewComponent {
/**
* Emit view component data has changed event
* View Component model
* Usually restored model from tab cache
*/
modelChange?: EventEmitter<any>;
model: any;
/**
* A callback method that performs custom init tab-ui, invoked immediately after tab has initialized.
*/
init?(): void;
afterTabActivated(): void;
/**
* Emit view component data has changed event
*/
modelChange: EventEmitter<any>;
//* If tab content can't not be saved,these value can be null
/**
* Edit page tab judge model has changed
*/
isFormChange?(): boolean;
/**
* Initial model for check form is change
*/
initialModel?: any;
/**
* Emit view component data has been saved
*/
afterSaved?: EventEmitter<any>;
}
/**
* Tab item.
@ -58,12 +69,13 @@ export type TabItem = {
/**
* Unique id,used for identify content
*/
id: string;
isFixed?: boolean;
uniqueName: string | PageUniqueName;
/**
* If true,will not cache tab content
* If the tab is fixed, it will not be replaced by other tab
*
* You can use double-click to fixed the tab prevent it from being replaced
*/
disabledCache?: boolean;
isFixed?: boolean;
/**
* Preview page or edit page
*/

View File

@ -130,23 +130,23 @@ export class EoTableProComponent implements OnInit, OnChanges {
this.nzData.push(eoDeepCopy(this.nzDataItem));
}
}
const hasQuoteKey = this.columns.some(col => col.key?.includes('.'));
if (hasQuoteKey && !this.setting.isEdit) {
const chains = this.columns
.filter(col => col.key?.includes('.'))
.map(val => {
const arr = val.key.split('.');
const valResult = {
arr,
str: val.key,
name: arr.at(-1)
};
return valResult;
});
this.nzData = generateQuoteKeyValue(chains, this.nzData, {
childKey: this.tableConfig.childKey
});
}
// const hasQuoteKey = this.columns.some(col => col.key?.includes('.'));
// if (hasQuoteKey && !this.setting.isEdit) {
// const chains = this.columns
// .filter(col => col.key?.includes('.'))
// .map(val => {
// const arr = val.key.split('.');
// const valResult = {
// arr,
// str: val.key,
// name: arr.at(-1)
// };
// return valResult;
// });
// this.nzData = generateQuoteKeyValue(chains, this.nzData, {
// childKey: this.tableConfig.childKey
// });
// }
}
}
getPureNzData() {

View File

@ -1,18 +1,16 @@
import { Component, OnInit } from '@angular/core';
import { has } from 'lodash-es';
import { ExtensionService } from 'pc/browser/src/app/services/extensions/extension.service';
import { Message, MessageService } from 'pc/browser/src/app/services/message';
import { MessageService } from 'pc/browser/src/app/services/message';
import { ApiService } from 'pc/browser/src/app/services/storage/api.service';
import { TraceService } from 'pc/browser/src/app/services/trace.service';
import { EXPORT_API } from 'pc/browser/src/app/shared/constans/featureName';
import { ExtensionChange } from 'pc/browser/src/app/shared/decorators';
import { FeatureInfo } from 'pc/browser/src/app/shared/models/extension-manager';
import { StoreService } from 'pc/browser/src/app/shared/store/state.service';
import StorageUtil from 'pc/browser/src/app/shared/utils/storage/storage.utils';
import { StoreService } from 'pc/browser/src/app/store/state.service';
import { Subject, takeUntil } from 'rxjs';
import { Subject } from 'rxjs';
// shit angular-cli 配不明白
// import { version } from '../../../../../../../../package.json' assert { type: 'json' };
import pkgInfo from '../../../../../../../package.json';
@Component({
@ -85,7 +83,7 @@ export class ExportApiComponent implements OnInit {
if (data) {
console.log('projectExport result', data);
try {
data.postcatVersion = pkgInfo.version;
data.postcat = pkgInfo.version;
let output = module[action]({ data: data || {} });
//Change format
if (has(output, 'status') && output.status === 0) {

View File

@ -9,7 +9,7 @@ import { TraceService } from 'pc/browser/src/app/services/trace.service';
import { IMPORT_API } from 'pc/browser/src/app/shared/constans/featureName';
import { ExtensionChange } from 'pc/browser/src/app/shared/decorators';
import { FeatureInfo } from 'pc/browser/src/app/shared/models/extension-manager';
import { StoreService } from 'pc/browser/src/app/store/state.service';
import { StoreService } from 'pc/browser/src/app/shared/store/state.service';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@ -67,7 +67,7 @@ export class ImportApiComponent implements OnInit {
constructor(
private router: Router,
private trace: TraceService,
private eoMessage: EoNgFeedbackMessageService,
private feedback: EoNgFeedbackMessageService,
private extensionService: ExtensionService,
private store: StoreService,
private apiService: ApiService,
@ -103,7 +103,7 @@ export class ImportApiComponent implements OnInit {
async submit(callback) {
StorageUtil.set('import_api_modal', this.currentExtension);
if (!this.uploadData) {
this.eoMessage.error($localize`Please import the file first`);
this.feedback.error($localize`Please import the file first`);
callback('stayModal');
return;
}
@ -116,16 +116,20 @@ export class ImportApiComponent implements OnInit {
const [data, err] = module[action](content);
console.log('import data', window.structuredClone?.(data));
if (err) {
this.eoMessage.error(err.msg);
this.feedback.error(err.msg);
console.error(err.msg);
callback(false);
callback('stayModal');
return;
}
try {
console.log('content', content);
data.collections = parseAndCheckCollections(data.collections);
data.environmentList = data.environmentList.filter(n => {
data.collections = parseAndCheckCollections(data.collections || []);
if (!data.collections?.length && !data.environmentList?.length) {
this.feedback.warning($localize`The imported file contains ${data.collections.length} APIs, which will be ignored`);
callback('stayModal');
return;
}
data.environmentList = (data.environmentList || []).filter(n => {
const { validate, data } = parseAndCheckEnv(n);
if (validate) {
return data;

View File

@ -1,4 +1,5 @@
import { CollectionTypeEnum, ImportProjectDto } from 'pc/browser/src/app/services/storage/db/dto/project.dto';
import { GroupModuleType, GroupType } from 'pc/browser/src/app/services/storage/db/dto/group.dto';
import { ImportProjectDto } from 'pc/browser/src/app/services/storage/db/dto/project.dto';
import { convertApiData } from '../../../services/storage/db/dataSource/convert';
@ -18,12 +19,12 @@ export const old2new = (params, projectUuid, workSpaceUuid): ImportProjectDto =>
if (item.uri) {
const newApiData = convertApiData(item);
Object.assign(item, newApiData);
item.collectionType = CollectionTypeEnum.API_DATA;
item.type = GroupType.virtual;
item.module = GroupModuleType.API;
}
// 分组
else {
item.collectionType = CollectionTypeEnum.GROUP;
item.type = GroupType.USER_CREATED;
if (item.children?.length) {
formatData(item.children);
}

View File

@ -23,7 +23,7 @@ export class PushApiComponent implements OnInit {
private destroy$: Subject<void> = new Subject<void>();
constructor(
private extensionService: ExtensionService,
private eoMessage: EoNgFeedbackMessageService,
private feedback: EoNgFeedbackMessageService,
private apiService: ApiService,
private messageService: MessageService
) {}
@ -63,6 +63,10 @@ export class PushApiComponent implements OnInit {
}
const action = feature.action || null;
const module = await this.extensionService.getExtensionPackage(this.currentExtension);
if (!module) {
callback(false);
return;
}
if (module?.[action] && typeof module[action] === 'function') {
const [data] = await this.apiService.api_projectExportProject({});
@ -70,7 +74,7 @@ export class PushApiComponent implements OnInit {
try {
const output = await module[action](data);
if (has(output, 'status') && output.status !== 0) {
this.eoMessage.error(output.message);
this.feedback.error(output.message);
callback('stayModal');
return;
}

View File

@ -26,7 +26,7 @@ export class ExtensionSelectComponent {
filename = '';
tipsMap = { ...featuresTipsMap, ...categoriesTipsMap };
constructor(private message: EoNgFeedbackMessageService) {}
constructor(private feedback: EoNgFeedbackMessageService) {}
selectExtension({ key, properties }) {
this.extensionChange.emit(key);
@ -44,7 +44,7 @@ export class ExtensionSelectComponent {
parserFile = file =>
new Observable((observer: Observer<boolean>) => {
if (file.type !== 'application/json') {
this.message.error($localize`Only files in JSON format are supported`);
this.feedback.error($localize`Only files in JSON format are supported`);
observer.complete();
return;
}
@ -55,7 +55,7 @@ export class ExtensionSelectComponent {
observer.complete();
})
.catch(err => {
this.message.error(err);
this.feedback.error(err);
});
});
}

View File

@ -2,16 +2,15 @@ import { Component, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@
import { EoNgFeedbackMessageService } from 'eo-ng-feedback';
import { debounce } from 'lodash-es';
import { ExtensionService } from 'pc/browser/src/app/services/extensions/extension.service';
import { Message, MessageService } from 'pc/browser/src/app/services/message';
import { ApiService } from 'pc/browser/src/app/services/storage/api.service';
import { TraceService } from 'pc/browser/src/app/services/trace.service';
import { EoSchemaFormComponent } from 'pc/browser/src/app/shared/components/schema-form/schema-form.component';
import { PULL_API } from 'pc/browser/src/app/shared/constans/featureName';
import { ExtensionChange } from 'pc/browser/src/app/shared/decorators';
import { FeatureInfo } from 'pc/browser/src/app/shared/models/extension-manager';
import { EffectService } from 'pc/browser/src/app/store/effect.service';
import { StoreService } from 'pc/browser/src/app/store/state.service';
import { Subject, takeUntil } from 'rxjs';
import { EffectService } from 'pc/browser/src/app/shared/store/effect.service';
import { StoreService } from 'pc/browser/src/app/shared/store/state.service';
import { Subject } from 'rxjs';
import { eoDeepCopy } from '../../../shared/utils/index.utils';
import { SYNC_API_SCHEMA } from './schema';
@ -43,9 +42,8 @@ export class SyncApiComponent implements OnInit, OnChanges {
private destroy$: Subject<void> = new Subject<void>();
constructor(
private extensionService: ExtensionService,
private eoMessage: EoNgFeedbackMessageService,
private feedback: EoNgFeedbackMessageService,
private apiService: ApiService,
private messageService: MessageService,
private store: StoreService,
private effectService: EffectService,
private trace: TraceService
@ -170,7 +168,7 @@ export class SyncApiComponent implements OnInit, OnChanges {
const [data, err] = await module[feature.action](this.validateForm?.value);
console.log('data', data, err);
if (err) {
this.eoMessage.error($localize`Sync API from URL error: ${err}`);
this.feedback.error($localize`Sync API from URL error: ${err?.message || err}`);
return 'stayModal';
}
// this.eoMessage.success($localize`Sync API from URL Successfully`);
@ -201,7 +199,7 @@ export class SyncApiComponent implements OnInit, OnChanges {
};
const [data, err] = await this.apiService[params.id ? 'api_projectUpdateSyncSetting' : 'api_projectCreateSyncSetting'](params);
if (err) {
this.eoMessage.error(err.msg);
this.feedback.error(err.msg);
console.error(err.msg);
callback?.('stayModal');
return;

View File

@ -3,7 +3,7 @@ import { EoNgFeedbackMessageService } from 'eo-ng-feedback';
import { autorun, reaction } from 'mobx';
import { TraceService } from 'pc/browser/src/app/services/trace.service';
import { StoreService } from '../../store/state.service';
import { StoreService } from '../../shared/store/state.service';
import { MemberService } from './member.service';
@Component({
@ -60,7 +60,7 @@ export class MemberListComponent implements OnInit {
constructor(
public store: StoreService,
private trace: TraceService,
private message: EoNgFeedbackMessageService,
private feedback: EoNgFeedbackMessageService,
public member: MemberService
) {}
@ -94,20 +94,20 @@ export class MemberListComponent implements OnInit {
async changeRole(item) {
const isOK: boolean = await this.member.changeRole(item);
if (isOK) {
this.message.success($localize`Change role successfully`);
this.feedback.success($localize`Change role successfully`);
this.trace.report('switch_member_permission');
this.queryList();
return;
}
this.message.error($localize`Change role Failed`);
this.feedback.error($localize`Change role Failed`);
}
async removeMember(item) {
const [data, err]: any = await this.member.removeMember(item);
if (err) {
this.message.error($localize`Change role error`);
this.feedback.error($localize`Change role error`);
return;
}
this.message.success($localize`Remove Member successfully`);
this.feedback.success($localize`Remove Member successfully`);
this.queryList();
}
}

View File

@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core';
import { NpsPositionDirective } from 'pc/browser/src/app/components/nps-mask/nps-mask-postion.directive';
import { APP_CONFIG } from 'pc/browser/src/environments/environment';
import { StoreService } from '../../../store/state.service';
import { StoreService } from '../../../shared/store/state.service';
@Component({
selector: 'pc-nps-mask',

View File

@ -3,7 +3,7 @@ import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators }
import { EoNgFeedbackMessageService } from 'eo-ng-feedback';
import { MessageService } from 'pc/browser/src/app/services/message/message.service';
import { ApiService } from 'pc/browser/src/app/services/storage/api.service';
import { StoreService } from 'pc/browser/src/app/store/state.service';
import { StoreService } from 'pc/browser/src/app/shared/store/state.service';
@Component({
selector: 'eo-account',
@ -63,7 +63,7 @@ export class AccountComponent implements OnInit {
public store: StoreService,
public message: MessageService,
public api: ApiService,
public eMessage: EoNgFeedbackMessageService
public feedback: EoNgFeedbackMessageService
) {
this.isSaveUsernameBtnLoading = false;
this.validatePasswordForm = UntypedFormGroup;
@ -92,10 +92,10 @@ export class AccountComponent implements OnInit {
password
});
if (err) {
this.eMessage.error($localize`Validation failed`);
this.feedback.error($localize`Validation failed`);
return;
}
this.eMessage.success($localize`Password reset success !`);
this.feedback.success($localize`Password reset success !`);
// * Clear password form
this.validatePasswordForm.reset();

View File

@ -49,7 +49,7 @@ export class DataStorageComponent implements OnInit {
constructor(
private fb: FormBuilder,
private message: EoNgFeedbackMessageService,
private feedback: EoNgFeedbackMessageService,
private messageS: MessageService,
private dataSource: DataSourceService,
private settingService: SettingService
@ -80,13 +80,13 @@ export class DataStorageComponent implements OnInit {
};
const isSuccess = await this.dataSource.pingCloudServerUrl(this.validateForm.value['backend.url']);
if (isSuccess) {
this.message.success($localize`Successfully connect to cloud`);
this.feedback.success($localize`Successfully connect to cloud`);
StorageUtil.set('IS_SHOW_DATA_SOURCE_TIP', 'false');
//Relogin to update user info
this.messageS.send({ type: 'login', data: {} });
this.saveConf();
} else {
this.message.error($localize`Failed to connect`);
this.feedback.error($localize`Failed to connect`);
}
}

View File

@ -1,8 +1,5 @@
import { Component } from '@angular/core';
import { EoNgFeedbackMessageService } from 'eo-ng-feedback';
import { MessageService } from 'pc/browser/src/app/services/message';
import { ApiService } from 'pc/browser/src/app/services/storage/api.service';
import { StoreService } from 'pc/browser/src/app/store/state.service';
import { DataSourceService } from '../../../services/data-source/data-source.service';
@ -46,13 +43,7 @@ import { DataSourceService } from '../../../services/data-source/data-source.ser
})
export class TokenComponent {
token;
constructor(
private api: ApiService,
private message: MessageService,
private eoMessage: EoNgFeedbackMessageService,
private store: StoreService,
private dataSource: DataSourceService
) {
constructor(private api: ApiService, private dataSource: DataSourceService) {
this.token = '';
}

View File

@ -5,6 +5,7 @@ import { SystemSettingComponent } from './system-setting.component';
export const LOCAL_SETTINGS_KEY = 'LOCAL_SETTINGS_KEY';
//TODO use StorageUtils to replace this
export const getSettings = () => {
try {
let result = JSON.parse(localStorage.getItem(LOCAL_SETTINGS_KEY) || '{}');

View File

@ -1,6 +1,6 @@
import { Component, Input } from '@angular/core';
import { AccountComponent } from 'pc/browser/src/app/components/system-setting/common/account.component';
import { StoreService } from 'pc/browser/src/app/store/state.service';
import { StoreService } from 'pc/browser/src/app/shared/store/state.service';
import { SettingItem } from '../eo-ui/setting/setting.component';
import { AboutComponent, LanguageSwticherComponent, SelectThemeComponent, TokenComponent } from './common';

View File

@ -20,7 +20,6 @@ export class LanguageService {
this.languages.find(val => window.location.pathname.includes(`/${val.path}/`))?.value ||
this.setting.settings?.['system.language'] ||
(navigator.language.includes('zh') ? 'zh-Hans' : 'en-US');
this.trace.setUser({ app_language: this.systemLanguage });
}
get langHash() {
return this.langHashMap.get(this.systemLanguage);
@ -28,6 +27,7 @@ export class LanguageService {
init() {
//System language First
this.changeLanguage(this.setting.settings?.['system.language']);
this.trace.setVisitor({ app_language: this.systemLanguage });
}
changeLanguage(localeID) {
if (!localeID || localeID === this.systemLanguage) {

View File

@ -22,11 +22,13 @@ export class NotificationService {
return;
}
this.modal.create({
stayWhenRouterChange: true,
nzTitle: $localize`Release Notes`,
nzContent: $localize`There will be downtime updates from ${logInfo.startTime.getHours()}\:00 to ${logInfo.endTime.getHours()}\:00 today, and may be temporarily inaccessible.`
});
StorageUtil.set('notification_has_show', true, 60 * 60 * 24);
//! safari may cause erro when the user first open page,it will show even if the time has passed
// this.modal.create({
// stayWhenRouterChange: true,
// nzTitle: $localize`Release Notes`,
// nzContent: $localize`There will be downtime updates from ${logInfo.startTime.getHours()}\:00 to ${logInfo.endTime.getHours()}\:00 today, and may be temporarily inaccessible.`
// });
// StorageUtil.set('notification_has_show', true, 60 * 60 * 24);
}
}

View File

@ -62,6 +62,9 @@ export class ThemeVariableService {
'buttonTextHoverText',
'menuItemText',
'tabsText',
/**
* Tabs Active Color is default color
*/
'tabsActiveText',
'tabsCardText',
'tabsCardItemActiveText',

View File

@ -7,9 +7,9 @@ import { PROTOCOL } from 'pc/browser/src/app/shared/models/protocol.constant';
import { APP_CONFIG } from 'pc/browser/src/environments/environment';
import packageJson from '../../../../../../../package.json';
import { StoreService } from '../../../shared/store/state.service';
import { getBrowserType } from '../../../shared/utils/browser-type';
import StorageUtil from '../../../shared/utils/storage/storage.utils';
import { StoreService } from '../../../store/state.service';
type DescriptionsItem = {
readonly id: string;
@ -59,7 +59,7 @@ export class WebService {
if (this.isWeb) {
this.settingService.putSettings({ 'backend.url': window.location.origin });
} else {
this.settingService.putSettings({ 'backend.url': APP_CONFIG.serverUrl });
this.settingService.putSettings({ 'backend.url': !APP_CONFIG.production ? window.location.origin : APP_CONFIG.serverUrl });
}
this.getClientResource();
}
@ -248,7 +248,7 @@ export class WebService {
systemInfo = window.electron.getSystemInfo();
descriptions.push(...electronDetails);
} else {
systemInfo = getBrowserType(getSettings()?.['system.language']);
systemInfo = getBrowserType();
descriptions.push(
...Object.entries<string>(systemInfo).map(([key, value]) => ({
id: key,

View File

@ -3,8 +3,8 @@ import { EoNgFeedbackMessageService } from 'eo-ng-feedback';
import { autorun } from 'mobx';
import { MessageService } from 'pc/browser/src/app/services/message';
import { IS_SHOW_REMOTE_SERVER_NOTIFICATION } from 'pc/browser/src/app/shared/models/storageKeys.constant';
import { EffectService } from 'pc/browser/src/app/store/effect.service';
import { StoreService } from 'pc/browser/src/app/store/state.service';
import { EffectService } from 'pc/browser/src/app/shared/store/effect.service';
import { StoreService } from 'pc/browser/src/app/shared/store/state.service';
import { StorageUtil } from '../../shared/utils/storage/storage.utils';
@ -34,7 +34,7 @@ export class LocalWorkspaceTipComponent implements OnInit {
@Output() readonly isShowChange: EventEmitter<boolean> = new EventEmitter<boolean>();
manualClose = StorageUtil.get(IS_SHOW_REMOTE_SERVER_NOTIFICATION) === 'false';
constructor(
private eoMessage: EoNgFeedbackMessageService,
private feedback: EoNgFeedbackMessageService,
private message: MessageService,
private store: StoreService,
private effect: EffectService
@ -54,7 +54,7 @@ export class LocalWorkspaceTipComponent implements OnInit {
const workspaces = this.store.getWorkspaceList;
if (workspaces.length === 1) {
// * only local workspace
this.eoMessage.warning($localize`You don't have cloud space yet, please new one`);
this.feedback.warning($localize`You don't have cloud space yet, please new one`);
this.message.send({ type: 'addWorkspace', data: {} });
return;
}

View File

@ -1,7 +1,7 @@
import { Component } from '@angular/core';
import { autorun } from 'mobx';
import { StoreService } from '../../../store/state.service';
import { StoreService } from '../../../shared/store/state.service';
@Component({
selector: 'eo-nav-breadcrumb',

View File

@ -1,11 +1,10 @@
import { Component, OnInit } from '@angular/core';
import { EffectService } from 'pc/browser/src/app/store/effect.service';
import { StoreService } from 'pc/browser/src/app/store/state.service';
import { Component } from '@angular/core';
import { EffectService } from 'pc/browser/src/app/shared/store/effect.service';
import { StoreService } from 'pc/browser/src/app/shared/store/state.service';
import { FeatureControlService } from '../../../../core/services/feature-control/feature-control.service';
import { DataSourceService } from '../../../../services/data-source/data-source.service';
import { MessageService } from '../../../../services/message';
import { ModalService } from '../../../../services/modal.service';
@Component({
selector: 'eo-select-workspace',

View File

@ -6,7 +6,7 @@ import { FeatureControlService } from '../../../core/services/feature-control/fe
import { DataSourceService } from '../../../services/data-source/data-source.service';
import { MessageService } from '../../../services/message';
import { ApiService } from '../../../services/storage/api.service';
import { StoreService } from '../../../store/state.service';
import { StoreService } from '../../../shared/store/state.service';
@Component({
selector: 'pc-btn-user',
@ -44,7 +44,7 @@ export class BtnUserComponent {
private message: MessageService,
private api: ApiService,
public feature: FeatureControlService,
private eMessage: EoNgFeedbackMessageService,
private feedback: EoNgFeedbackMessageService,
public store: StoreService,
private dataSourceService: DataSourceService,
private setting: SettingService
@ -57,7 +57,7 @@ export class BtnUserComponent {
}
async loginOut() {
this.store.clearAuth();
this.eMessage.success($localize`Successfully logged out !`);
this.feedback.success($localize`Successfully logged out !`);
const [, err]: any = await this.api.api_userLogout({});
if (err) {
return;

View File

@ -1,8 +1,8 @@
import { Component } from '@angular/core';
import { EoNgFeedbackMessageService } from 'eo-ng-feedback';
import { EffectService } from 'pc/browser/src/app/shared/store/effect.service';
import { StoreService } from 'pc/browser/src/app/shared/store/state.service';
import { copy } from 'pc/browser/src/app/shared/utils/index.utils';
import { EffectService } from 'pc/browser/src/app/store/effect.service';
import { StoreService } from 'pc/browser/src/app/store/state.service';
import { interval } from 'rxjs';
import { DataSourceService } from '../../services/data-source/data-source.service';
@ -52,12 +52,12 @@ export class GetShareLinkComponent {
private effect: EffectService,
public store: StoreService,
public dataSourceService: DataSourceService,
private message: EoNgFeedbackMessageService
private feedback: EoNgFeedbackMessageService
) {}
handleGetShareLink() {
this.dataSourceService.checkRemoteCanOperate(async () => {
if (this.store.isLocal) {
this.message.info($localize`If you want to share API,Please switch to cloud workspace`);
this.feedback.info($localize`If you want to share API,Please switch to cloud workspace`);
}
this.link = await this.effect.updateShareLink();
});

View File

@ -2,7 +2,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
import { NzModalService } from 'ng-zorro-antd/modal';
import { ExtensionComponent } from 'pc/browser/src/app/pages/components/extension/extension.component';
import { MessageService } from 'pc/browser/src/app/services/message';
import { StoreService } from 'pc/browser/src/app/store/state.service';
import { StoreService } from 'pc/browser/src/app/shared/store/state.service';
import { APP_CONFIG } from 'pc/browser/src/environments/environment';
import { interval, Subject, takeUntil } from 'rxjs';
import { distinct } from 'rxjs/operators';

View File

@ -1,7 +1,7 @@
import { NgModule } from '@angular/core';
import { NzBreadCrumbModule } from 'ng-zorro-antd/breadcrumb';
import { DownloadClientComponent } from 'pc/browser/src/app/components/download-client/download-client.component';
import { DownloadClientModule } from '../../components/download-client/download-client.module';
import { LogoModule } from '../../components/logo/logo.module';
import { SharedModule } from '../../shared/shared.module';
import { NavBreadcrumbComponent } from './breadcrumb/nav-breadcrumb.component';
@ -14,7 +14,7 @@ import { NavbarComponent } from './navbar.component';
import { ShareNavbarComponent } from './share-navbar/share-navbar.component';
@NgModule({
imports: [SharedModule, DownloadClientModule, LogoModule, NzBreadCrumbModule],
imports: [SharedModule, DownloadClientComponent, LogoModule, NzBreadCrumbModule],
declarations: [
NavbarComponent,
GetShareLinkComponent,

View File

@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core';
import { APP_CONFIG } from 'pc/browser/src/environments/environment';
import { ApiService } from '../../../services/storage/api.service';
import { StoreService } from '../../../store/state.service';
import { StoreService } from '../../../shared/store/state.service';
@Component({
selector: 'pc-share-navbar',

View File

@ -9,10 +9,10 @@
}
.eo-sidebar-shrink {
width: 50px;
width: var(--layout-sidebar-shrink-width);
.sidebar-item {
height: 50px;
height: var(--layout-sidebar-shrink-width);
}
}

View File

@ -6,7 +6,7 @@ import { Message, MessageService } from 'pc/browser/src/app/services/message';
import { SIDEBAR_VIEW } from 'pc/browser/src/app/shared/constans/featureName';
import { ExtensionChange, ExtensionMessage } from 'pc/browser/src/app/shared/decorators';
import { ExtensionInfo } from 'pc/browser/src/app/shared/models/extension-manager';
import { StoreService } from 'pc/browser/src/app/store/state.service';
import { StoreService } from 'pc/browser/src/app/shared/store/state.service';
import { Subscription } from 'rxjs';
import { filter } from 'rxjs/operators';

View File

@ -1,5 +1,6 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { ChatgptRobotComponent } from 'pc/browser/src/app/pages/components/chatgpt-robot/chatgpt-robot.component';
import { SharedModule } from '../../shared/shared.module';
import { ToolbarComponent } from './toolbar.component';

View File

@ -5,13 +5,11 @@ import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { EoNgButtonModule } from 'eo-ng-button';
import GPT3Tokenizer from 'gpt3-tokenizer';
import { FeatureControlService } from 'pc/browser/src/app/core/services/feature-control/feature-control.service';
import { Message, MessageService } from 'pc/browser/src/app/services/message';
import { TraceService } from 'pc/browser/src/app/services/trace.service';
import { FEATURE_CONTROL } from 'pc/browser/src/app/shared/constans/featureName';
import { ExtensionChange, ExtensionMessage } from 'pc/browser/src/app/shared/decorators';
import { ExtensionInfo } from 'pc/browser/src/app/shared/models/extension-manager';
import { StoreService } from 'pc/browser/src/app/shared/store/state.service';
import StorageUtil from 'pc/browser/src/app/shared/utils/storage/storage.utils';
import { StoreService } from 'pc/browser/src/app/store/state.service';
import { APP_CONFIG } from 'pc/browser/src/environments/environment';
import { ChatRobotModule } from '../../../components/chat-robot/chat-robot.module';
@ -106,7 +104,6 @@ export class ChatgptRobotComponent implements OnInit {
private http: HttpClient,
public chat: ChatRobotService,
public feature: FeatureControlService,
private message: MessageService,
private trace: TraceService,
private store: StoreService
) {}

View File

@ -20,7 +20,7 @@ export class ExtensionSettingComponent implements OnInit {
@Input() extName: string;
localSettings = {} as Record<string, any>;
constructor(private settingService: SettingService, private message: EoNgFeedbackMessageService) {}
constructor(private settingService: SettingService, private feedback: EoNgFeedbackMessageService) {}
ngOnInit(): void {
this.init();
@ -31,6 +31,6 @@ export class ExtensionSettingComponent implements OnInit {
}
handleSave = () => {
this.settingService.saveSetting(this.localSettings);
this.message.success($localize`Save Success`);
this.feedback.success($localize`Save Success`);
};
}

View File

@ -32,7 +32,7 @@
</nz-space>
</div>
</div>
<div class="flex items-center" *ngIf="isAvailableElectron">
<div class="flex items-center" *ngIf="isAvailablePlatform && isAvailableVersion">
<div *ngIf="extensionDetail?.installed" class="mr-[20px]">
<eo-ng-switch
class="mr-[3px]"
@ -59,7 +59,13 @@
<span *ngIf="!extensionDetail?.installed" i18n>Install</span>
</button>
</div>
<div class="flex flex-col" *ngIf="!isAvailableElectron">
<div class="flex flex-col items-end" *ngIf="!isAvailableVersion">
<pc-download-client i18n-title title="Upgrade"></pc-download-client>
<p class="text-[12px] mt-[10px] text-tips" i18n
>* You need to upgrade the client to {{ extensionDetail.engines?.postcat }} first</p
>
</div>
<div class="flex flex-col items-end" *ngIf="!isAvailablePlatform && isAvailableVersion">
<a [href]="APP_CONFIG.serverUrl" target="_bank"><button eo-ng-button nzType="primary" i18n>Jump to Use</button></a>
<p class="text-[12px] mt-[10px] text-tips" i18n>* This exension only support web</p>
</div>

View File

@ -1,10 +1,12 @@
import { Component, OnInit, Output, EventEmitter, Input } from '@angular/core';
import { compareVersions } from 'compare-versions';
import { ElectronService } from 'pc/browser/src/app/core/services';
import { LanguageService } from 'pc/browser/src/app/core/services/language/language.service';
import { TraceService } from 'pc/browser/src/app/services/trace.service';
import { ExtensionInfo } from 'pc/browser/src/app/shared/models/extension-manager';
import { APP_CONFIG } from 'pc/browser/src/environments/environment';
import pkgInfo from '../../../../../../../../package.json';
import { WebService } from '../../../../core/services/web/web.service';
import { ExtensionService } from '../../../../services/extensions/extension.service';
import { EoExtensionInfo } from '../extension.model';
@ -20,13 +22,16 @@ export class ExtensionDetailComponent implements OnInit {
@Input() nzSelectedIndex = 0;
isOperating = false;
introLoading = false;
isAvailableElectron = true;
changelogLoading = false;
isNotLoaded = true;
extensionDetail: EoExtensionInfo;
readonly APP_CONFIG = APP_CONFIG;
changeLog = '';
changeLogNotFound = false;
isAvailableVersion = true;
isAvailablePlatform = true;
constructor(
public extensionService: ExtensionService,
private webService: WebService,
@ -68,7 +73,8 @@ export class ExtensionDetailComponent implements OnInit {
this.introLoading = false;
this.isNotLoaded = false;
this.extensionDetail.introduction ||= $localize`This plugin has no documentation yet.`;
this.isAvailableElectron = this.checkisAvailableElectron(this.extensionDetail);
this.isAvailableVersion = compareVersions(pkgInfo.version, this.extensionDetail.engines?.postcat) >= 0 ? true : false;
this.isAvailablePlatform = this.checkisAvailablePlatform(this.extensionDetail);
this.fetchChangelog(this.language.systemLanguage);
setTimeout(() => {
@ -140,7 +146,13 @@ ${log}
this.fetchChangelog();
}
};
checkisAvailableElectron(pkgInfo): boolean {
/**
* Check current extension is available in current platform
*
* @param pkgInfo
* @returns
*/
checkisAvailablePlatform(pkgInfo): boolean {
if (this.electron.isElectron && this.extensionDetail.browser && !this.extensionDetail.main) return false;
return true;
}

View File

@ -1,21 +1,13 @@
<section class="flex-shrink-0 p-0 left tree-sider">
<div class="m-[10px]">
<!-- <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
></ng-template>
</div>
<!-- Fixed Group -->
<eo-ng-tree-default

View File

@ -1,4 +1,4 @@
@import '../../workspace/project/api/components/group/tree/api-group-tree.component';
@import '../../workspace/project/api/components/group/api-group-tree.component';
:host ::ng-deep {
display: flex;
@ -9,6 +9,10 @@
height: calc(62vh - 32px);
overflow: auto;
display: block;
.ant-tree-switcher {
display: none;
}
}
eo-ng-auto-complete input {

View File

@ -33,10 +33,6 @@ export const featuresTipsMap = {
name: $localize`format`,
suggest: '@feature:pushAPI'
},
syncAPI: {
name: $localize`format`,
suggest: '@feature:syncAPI'
},
pullAPI: {
name: $localize`format`,
suggest: '@feature:pullAPI'

View File

@ -10,6 +10,7 @@ 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 { DownloadClientComponent } from 'pc/browser/src/app/components/download-client/download-client.component';
import { ExtensionDetailComponent } from 'pc/browser/src/app/pages/components/extension/detail/extension-detail.component';
import { DownloadCountFormaterPipe } from 'pc/browser/src/app/pages/components/extension/download-count-formater.pipe';
import { SharedModule } from 'pc/browser/src/app/shared/shared.module';
@ -31,6 +32,7 @@ import { ExtensionListComponent } from './list/extension-list.component';
EoNgSwitchModule,
EoNgTreeModule,
NzResultModule,
DownloadClientComponent,
ShadowDomEncapsulationModule,
NzTagModule,
EoNgAutoCompleteModule,

View File

@ -53,9 +53,6 @@ export class ExtensionListComponent implements OnInit {
// 避免频繁切换,导致侧边栏选中状态与右侧展示不一致
if (originType === this.type) {
this.extensionList = data;
this.extensionList.sort((a, b) => {
return a.name === 'postcat-chat-robot' ? -1 : 1;
});
}
});
}

View File

@ -0,0 +1,31 @@
### Postcat 是一个强大的免费、跨平台、可扩展的 API 工具!
**和 Postman 等产品相比,它拥有以下备受喜爱的特性:**
1.❤️ 免费的团队协作:好的产品就应该更多人一起使用,我们不限制免费人数!
2.🚀 极具扩展性的插件系统:一键安装 ChatGPT、主题等各类插件来定制属于你的 API 开发工具~
3.😊 优秀的用户体验:一切都可以更简单,让我们的工作更高效~
**当然,我们还包括众多实用的基本功能:**
1.🍩 多协议支持REST、Websocket 等协议(即将支持 GraphQL、gRPC、TCP、UDP
2.📕 API 设计、文档展示、分享
3.⚡ API 测试
4.🎭 Mock Server
5.🙌 环境管理
...

View File

@ -0,0 +1,25 @@
::ng-deep {
.model-article {
.ant-modal-footer {
text-align: center;
}
h3 {
margin-top: 20px;
font-weight: bold;
color: var(--primary-color) !important;
}
p {
display: block;
font-size: 12px;
line-height: 1.5rem !important;
margin: 5px;
}
strong {
display: inline-block;
margin-top: 20px !important;
}
}
}

View File

@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NewbieGuideComponent } from './newbie-guide.component';
describe('NewbieGuideComponent', () => {
let component: NewbieGuideComponent;
let fixture: ComponentFixture<NewbieGuideComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [NewbieGuideComponent]
}).compileComponents();
fixture = TestBed.createComponent(NewbieGuideComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,23 @@
import { Component } from '@angular/core';
import markdownIt from 'markdown-it/dist/markdown-it';
import NEWBIE_GUIDE from 'pc/browser/src/app/shared/constans/newbie-guide';
@Component({
standalone: true,
selector: 'pc-newbie-guide',
template: `
<div class="newbie-guide">
<img src="assets/images/newbie-guide.png" style="width: 100%" alt="" />
<div id="newbie-guide-markdown"></div>
</div>
`,
styleUrls: ['./newbie-guide.component.scss']
})
export class NewbieGuideComponent {
async ngAfterViewInit() {
let md = new markdownIt();
const newbieGuideHtml = md.render(NEWBIE_GUIDE);
document.getElementById('newbie-guide-markdown').innerHTML = newbieGuideHtml;
}
}

View File

@ -0,0 +1,24 @@
::ng-deep {
.model-article {
.ant-modal-footer {
text-align: center;
}
h3 {
margin-top: 20px;
font-weight: bold;
color: var(--primary-color) !important;
}
p {
display: block;
font-size: 12px;
line-height: 30px !important;
}
strong {
display: inline-block;
margin-top: 20px !important;
}
}
}

View File

@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UpdateLogComponent } from './update-log.component';
describe('UpdateLogComponent', () => {
let component: UpdateLogComponent;
let fixture: ComponentFixture<UpdateLogComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [UpdateLogComponent]
}).compileComponents();
fixture = TestBed.createComponent(UpdateLogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,24 @@
import { Component } from '@angular/core';
import markdownIt from 'markdown-it/dist/markdown-it';
import UPDATE_LOG from 'pc/browser/src/app/shared/constans/update-log';
@Component({
selector: 'pc-update-log',
template: `
<div class="update-log">
<div id="update-log-markdown"></div>
</div>
`,
styleUrls: ['./update-log.component.scss']
})
export class UpdateLogComponent {
async ngAfterViewInit() {
let md = new markdownIt();
const html = md.render(UPDATE_LOG);
const updateLogHtml = html.replace(/<img [^>]*src=['"]([^'"]+)[^>]*>/gi, match =>
match.replace(/<img /gi, '<img style="width: 100%" ')
);
document.getElementById('update-log-markdown').innerHTML = updateLogHtml;
}
}

View File

@ -0,0 +1,164 @@
# Postcat API 客户端Client
![Postcat API Client](http://data.eolinker.com/course/QbLMSaJ7f3dcd0b075a7031b31f8acb486e0a090f1bdc8d.jpeg)
<p align="center"><a href="wiki/README.en.md">English</a> | <span>简体中文</span></p>
<p align="center">
<a href="https://github.com/Postcatlab/postcat"><img src="https://img.shields.io/github/license/Postcatlab/postcat?sanitize=true" alt="License"></a>
<a href="https://github.com/Postcatlab/postcat/releases"><img src="https://img.shields.io/github/v/release/Postcatlab/postcat?sanitize=true" alt="Version"></a>
<a href="https://github.com/Postcatlab/postcat/releases"><img src="https://img.shields.io/github/downloads/Postcatlab/postcat/total?sanitize=true" alt="Downloads"></a>
<a href="https://discord.gg/W3uk39zJCR"><img src="https://img.shields.io/badge/chat-on%20discord-7289da.svg?sanitize=true" alt="Chat"></a>
</p>
## 概述
**Postcat** 是一个强大的开源、免费的、跨平台Windows、Mac、Linux、Browsers...)的 **API 开发测试工具**,支持 REST、Websocket 等协议(即将支持 GraphQL、gRPC、TCP、UDP帮助你加速完成 API 开发和测试工作。它非常适合中小团队及个人使用。
![Postcat UI](https://data.eolink.com/ImGzhCi79d0beb5b8221670dffceb61bf642af1960d3881)
我们在保证 **Postcat** 轻巧灵活的同时,还为它设计了一个强大的插件系统,让您可以一键使用插件来增强它的功能。
![Postcat Extensions](https://data.eolink.com/22UMwcV01e087e3549edb91361f15a9ba8047e16d0d3f3f)
因此 **Postcat** 理论上是一个拥有无限可能的 API 产品可以从Logo 中看到,我们也形象地为它加上了一件披风,代表它的无限可能。
## 免登录在线使用或下载
**Postcat** 现在已经支持 Windows、Mac、Linux等系统你可以通过以下地址访问并下载。同时我们也提供了 Web 端,方便你在任何浏览器上使用。
**[https://postcat.com/](https://postcat.com//)**
如果您试用之后觉得不错,**请给我们的Postcat一个 Star 和 Fork~**你的支持是我们不断改进产品的动力!
## 详细的文档
[Postcat 文档](https://docs.postcat.com/)
[插件开发文档](https://developer.postcat.com/api/get-started.html)
## 功能特性和迭代计划Roadmap
- 🚀 多协议支持
-- 已实现HTTP REST、Websocket
-- 即将实现GraphQL、TCP、UDP、gRPC
- 📕 API 文档
- ✨ API 设计
- ⚡ API 测试
- 🎭 Mock
- 🙌 团队协作
- 🎈 文档分享
- 🗺 环境
- 🧶 全局变量
- 🧩 自定义主题风格
- 🌐 多语言支持中文、English
了解更多具体迭代计划:[Github Project](https://github.com/orgs/Postcatlab/projects/3)
</br>也欢迎给我们多多提需求~
</br>
## Bug 和需求反馈
如果想要反馈 Bug、提供产品意见可以创建一个 [Github issue](https://github.com/Postcatlab/postcat/issues) 联系我们,十分感谢!
如果您希望和 Postcat 团队近距离交流,讨论产品使用技巧以及了解更多产品最新进展,欢迎加入以下渠道。
- QQ群号码981965807
- QQ群链接[加入Postcat 用户群](https://jq.qq.com/?_wv=1027&k=Kej1qTUy)
- 微信群:
![](http://data.eolinker.com/course/NKhRRF668370911c8b8ea8a0887b5d62e71b0f1a22ad76a.png)
## 开发 Postcat
<details>
<summary>运行代码</summary>
</br>
请确保你已经部署好所需的开发环境:
- Node.js >= 14.17.x
- yarn >= 1.22.x
我们在开发和构建时使用 yarn 作为包管理工具,强烈建议你也这么做,但如果您希望使用 npm 也完全没问题,只是在安装依赖时可能需要多花一些时间。
### 运行桌面端程序
```shell
yarn install
yarn start
```
### 运行浏览器程序
```shell
cd src/browser&&npm install
yarn start
```
### 提高效率
如果想提高开发效率,可以安装 Angular 官方提供的命令行 Angular-cli 快速生成组件、服务等模板。
```
yarn add @angular/cli --global
```
</details>
<details>
<summary>内置命令</summary>
### 运行命令
|命令 |描述 |
| ------------ | ------------ |
|yarn start |开发模式下,同时运行在浏览器和桌面端 |
|yarn start:zh|中文开发模式,同时运行在浏览器和桌面端|
|yarn start:web |仅运行在浏览器,同时开启后端代理 |
|yarn start:electron|仅运行在桌面端 |
> 本项目 i18n 使用的是编译手段,所以开发时无法切换语言
### 打包构建
|命令 |描述 |
| ------------ | ------------ |
|sudo yarn build|各系统打包 Electron 应用 |
### 运行测试
|命令 |描述 |
| ------------ | ------------ |
|yarn test |执行单元测试 |

View File

@ -3,7 +3,7 @@ import { autorun, reaction } from 'mobx';
import { WebService } from 'pc/browser/src/app/core/services';
import { LanguageService } from 'pc/browser/src/app/core/services/language/language.service';
import { ApiService } from 'pc/browser/src/app/services/storage/api.service';
import { StoreService } from 'pc/browser/src/app/store/state.service';
import { StoreService } from 'pc/browser/src/app/shared/store/state.service';
import { APP_CONFIG } from 'pc/browser/src/environments/environment';
// * type(0=wechat, 1=qq, 2=github, 3=feishu, 4=corp_wechat, 5=ding_talk, 6=oauth2)
@ -60,15 +60,13 @@ export class ThirdLoginComponent implements OnInit {
isLoginBtnBtnLoading = false;
constructor(private api: ApiService, private web: WebService, public lang: LanguageService) {}
ngOnInit() {
autorun(() => {
this.renderList =
this.lang.langHash === 'zh'
? [
// { logo: 'feishu.png', label: '飞书', type: 'feishu' },
{ logo: 'github.png', label: 'Github', type: 'github' }
]
: [];
});
this.renderList =
this.lang.langHash === 'zh'
? [
// { logo: 'feishu.png', label: '飞书', type: 'feishu' },
{ logo: 'github.png', label: 'Github', type: 'github' }
]
: [];
}
logoLink(name) {
return `url('./assets/images/${name}')`;

View File

@ -9,8 +9,8 @@ import { ApiService } from 'pc/browser/src/app/services/storage/api.service';
import { LocalService } from 'pc/browser/src/app/services/storage/local.service';
import { RemoteService } from 'pc/browser/src/app/services/storage/remote.service';
import { TraceService } from 'pc/browser/src/app/services/trace.service';
import { EffectService } from 'pc/browser/src/app/store/effect.service';
import { StoreService } from 'pc/browser/src/app/store/state.service';
import { EffectService } from 'pc/browser/src/app/shared/store/effect.service';
import { StoreService } from 'pc/browser/src/app/shared/store/state.service';
import { interval, Subject } from 'rxjs';
import { distinct, takeUntil } from 'rxjs/operators';
@ -185,7 +185,7 @@ export class UserModalComponent implements OnInit, OnDestroy {
public store: StoreService,
public message: MessageService,
public api: ApiService,
public eMessage: EoNgFeedbackMessageService,
public feedback: EoNgFeedbackMessageService,
public effect: EffectService,
public dataSource: DataSourceService,
public modal: ModalService,
@ -235,19 +235,19 @@ export class UserModalComponent implements OnInit, OnDestroy {
if (this.store.isLocal) {
return;
}
this.eMessage.error($localize`Oops, server fail`);
this.feedback.error($localize`Oops, server fail`);
return;
}
if (type === 'ping-fail') {
this.eMessage.error($localize`Connect failed`);
this.feedback.error($localize`Connect failed`);
// * 唤起弹窗
this.isCheckConnectModalVisible = true;
return;
}
if (type === 'ping-success') {
this.eMessage.success($localize`Connect success`);
this.feedback.success($localize`Connect success`);
return;
}
@ -403,7 +403,7 @@ export class UserModalComponent implements OnInit, OnDestroy {
const isOk = this.validateLoginForm.valid;
if (!isOk) {
this.eMessage.error($localize`Please check you username or password`);
this.feedback.error($localize`Please check you username or password`);
return;
}
this.store.clearAuth();
@ -413,10 +413,10 @@ export class UserModalComponent implements OnInit, OnDestroy {
const [data, err]: any = await this.api.api_userLogin(formData);
if (err) {
if (err.code === 131000001) {
this.eMessage.error($localize`Username must a email`);
this.feedback.error($localize`Username must a email`);
return;
}
this.eMessage.error($localize`Please check you username or password`);
this.feedback.error($localize`Please check you username or password`);
return;
}
this.trace.setUserID(data.userId);
@ -493,10 +493,10 @@ export class UserModalComponent implements OnInit, OnDestroy {
// ! Attention: data is array
const [data, err]: any = await this.remote.api_workspaceCreate({ titles: [titles] });
if (err) {
this.eMessage.error($localize`New workspace Failed !`);
this.feedback.error($localize`New workspace Failed !`);
return;
}
this.eMessage.success($localize`New workspace successfully !`);
this.feedback.success($localize`New workspace successfully !`);
this.trace.report('add_workspace_success');
const workspace = data.at(0);
// * 关闭弹窗
@ -564,7 +564,7 @@ export class UserModalComponent implements OnInit, OnDestroy {
}))
});
if (err) {
this.eMessage.error($localize`Create Project Failed !`);
this.feedback.error($localize`Create Project Failed !`);
return;
}

View File

@ -1,7 +1,11 @@
import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { NzNotificationRef, NzNotificationService } from 'ng-zorro-antd/notification';
import { ElectronService } from 'pc/browser/src/app/core/services';
import { ElectronService, WebService } from 'pc/browser/src/app/core/services';
import { NewbieGuideComponent } from 'pc/browser/src/app/pages/components/model-article/newbie-guide/newbie-guide.component';
import { UpdateLogComponent } from 'pc/browser/src/app/pages/components/model-article/update-log/update-log.component';
import { ModalService } from 'pc/browser/src/app/services/modal.service';
import { StoreService } from 'pc/browser/src/app/shared/store/state.service';
import { filter } from 'rxjs';
import { SidebarService } from '../layouts/sidebar/sidebar.service';
@ -19,12 +23,16 @@ export class PagesComponent implements OnInit {
hasShowCookieTips = StorageUtil.get('has_show_cookie_tips');
isShowNotification;
sidebarViews: any[] = [];
constructor(
private socket: SocketService,
public electron: ElectronService,
private router: Router,
private sidebar: SidebarService,
private notification: NzNotificationService
private notification: NzNotificationService,
private modal: ModalService,
private store: StoreService,
private web: WebService
) {}
ngOnInit(): void {
// * 通过 socketIO 告知 Node 端,建立 grpc 连接
@ -40,6 +48,18 @@ export class PagesComponent implements OnInit {
// this.showCookiesTips();
// }
}
ngAfterContentInit() {
// TODO: first use
const result = this.web.getSystemInfo();
const version = result.shift().value;
if (!this.electron.isElectron && !this.store.getAppHasInitial && !StorageUtil.get('version')) {
this.newbieGuide(version);
return;
}
// if (StorageUtil.get('version') && StorageUtil.get('version') === version) return;
// this.updateLog(version);
}
closeNotification() {
this.notification.remove(this.cookieNotification.messageId);
}
@ -50,6 +70,7 @@ export class PagesComponent implements OnInit {
nzPauseOnHover: true
});
}
initSidebarVisible(url: string) {
if (['home/workspace/overview'].find(val => url.includes(val))) {
this.sidebar.visible = false;
@ -57,4 +78,38 @@ export class PagesComponent implements OnInit {
this.sidebar.visible = true;
}
}
newbieGuide(version: string) {
this.modal.create({
nzTitle: $localize`Welcome to Postcat`,
nzWidth: '650px',
nzContent: NewbieGuideComponent,
nzCancelText: $localize`Got it`,
nzBodyStyle: {
height: 'calc(100vh* 0.7)',
'overflow-y': 'scroll'
},
nzCentered: true,
nzClassName: 'model-article',
stayWhenRouterChange: true
});
StorageUtil.set('version', version);
}
updateLog(version: string) {
this.modal.create({
nzTitle: $localize`Release Log`,
nzWidth: '650px',
nzContent: UpdateLogComponent,
nzCancelText: $localize`Got it`,
nzBodyStyle: {
height: 'calc(100vh* 0.7)',
'overflow-y': 'scroll'
},
nzCentered: true,
nzClassName: 'model-article',
stayWhenRouterChange: true
});
StorageUtil.set('version', version);
}
}

View File

@ -17,9 +17,9 @@ import { PagesComponent } from './pages.component';
@NgModule({
declarations: [PagesComponent, SidebarComponent, LocalWorkspaceTipComponent, UserModalComponent, ThirdLoginComponent],
exports: [],
providers: [],
schemas: [],
exports: [],
imports: [
ChatgptRobotComponent,
PagesRoutingModule,

View File

@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
import { Observable } from 'rxjs';
import { StoreService } from '../../store/state.service';
import { StoreService } from '../../shared/store/state.service';
@Injectable()
export class RedirectSharedID implements CanActivate {

View File

@ -1,15 +1,27 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { API_TABS } from 'pc/browser/src/app/pages/workspace/project/api/api-tab.service';
import { ApiModule } from 'pc/browser/src/app/pages/workspace/project/api/api.module';
import { BASIC_TABS_INFO, TabsConfig } from 'pc/browser/src/app/pages/workspace/project/api/constants/api.model';
import { NavbarModule } from '../../layouts/navbar/navbar.module';
import { SharedModule } from '../../shared/shared.module';
import { ShareRoutingModule } from './share-routing.module';
import { ShareComponent } from './view/share-project.component';
const tabs = API_TABS.map(val => ({ ...val, pathname: `/share${val.pathname}` }));
@NgModule({
imports: [ShareRoutingModule, NavbarModule, CommonModule, SharedModule, ApiModule],
declarations: [ShareComponent],
providers: []
providers: [
{
provide: BASIC_TABS_INFO,
useValue: {
BASIC_TABS: tabs,
pathByName: tabs.reduce((acc, curr) => ({ ...acc, [curr.uniqueName]: curr.pathname }), {})
} as TabsConfig
}
]
})
export class ShareProjectModule {}

View File

@ -1,6 +1,6 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { GroupComponent } from 'pc/browser/src/app/pages/workspace/project/api/group/group.component';
import { GroupComponent } from 'pc/browser/src/app/pages/workspace/project/api/group-edit/group.component';
import { ShareComponent } from './view/share-project.component';
@ -24,6 +24,14 @@ const routes: Routes = [
{
path: 'test',
loadChildren: () => import('../workspace/project/api/http/test/api-test.module').then(m => m.ApiTestModule)
},
{
path: 'case',
loadChildren: () => import('../workspace/project/api/http/test/api-test.module').then(m => m.ApiTestModule)
},
{
path: 'mock',
loadChildren: () => import('../workspace/project/api/http/mock/mock.module').then(m => m.MockModule)
}
]
},

View File

@ -1,6 +1,6 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { StoreService } from 'pc/browser/src/app/store/state.service';
import { StoreService } from 'pc/browser/src/app/shared/store/state.service';
@Component({
selector: 'eo-share',

View File

@ -5,8 +5,8 @@ import { autorun } from 'mobx';
import { ApiService } from 'pc/browser/src/app/services/storage/api.service';
import { ModalService } from '../../../../services/modal.service';
import { EffectService } from '../../../../store/effect.service';
import { StoreService } from '../../../../store/state.service';
import { EffectService } from '../../../../shared/store/effect.service';
import { StoreService } from '../../../../shared/store/state.service';
@Component({
selector: 'eo-workspace-setting',
@ -78,7 +78,7 @@ export class WorkspaceSettingComponent {
];
constructor(
private fb: FormBuilder,
private message: EoNgFeedbackMessageService,
private feedback: EoNgFeedbackMessageService,
private api: ApiService,
private store: StoreService,
private modal: ModalService,
@ -119,10 +119,10 @@ export class WorkspaceSettingComponent {
workSpaceUuids: [wid]
});
if (err) {
this.message.error($localize`Delete failed !`);
this.feedback.error($localize`Failed to delete`);
return;
}
this.message.success($localize`Delete Succeeded`);
this.feedback.success($localize`Successfully deleted`);
await this.effect.updateWorkspaceList();
await this.effect.switchWorkspace(this.store.getLocalWorkspace.workSpaceUuid);
}
@ -146,10 +146,10 @@ export class WorkspaceSettingComponent {
title
});
if (err) {
this.message.error($localize`Edit workspace failed`);
this.feedback.error($localize`Edit workspace failed`);
return;
}
this.message.success($localize`Edit workspace successfully !`);
this.feedback.success($localize`Edit workspace successfully !`);
//Rest Current Workspace
await this.effect.updateWorkspaceList();

View File

@ -7,7 +7,7 @@ import { TraceService } from 'pc/browser/src/app/services/trace.service';
import { MemberListComponent } from '../../../../components/member-list/member-list.component';
import { MessageService } from '../../../../services/message/message.service';
import { StoreService } from '../../../../store/state.service';
import { StoreService } from '../../../../shared/store/state.service';
@Component({
selector: 'eo-workspace-member',
@ -95,7 +95,7 @@ export class WorkspaceMemberComponent implements OnInit {
isInvateModalVisible = false;
constructor(
public store: StoreService,
private eMessage: EoNgFeedbackMessageService,
private feedback: EoNgFeedbackMessageService,
public member: MemberService,
private message: MessageService,
private trace: TraceService,
@ -150,17 +150,17 @@ export class WorkspaceMemberComponent implements OnInit {
const btnSelectRunning = async () => {
const userIds = this.userCache;
if (userIds.length === 0) {
this.eMessage.error($localize`Please select a member`);
this.feedback.error($localize`Please select a member`);
return;
}
const [aData, aErr]: any = await this.member.addMember(userIds);
if (aErr) {
this.eMessage.error($localize`Add member failed`);
this.feedback.error($localize`Add member failed`);
return;
}
this.trace.report('add_workspace_member_success');
this.eMessage.success($localize`Add member successfully`);
this.feedback.success($localize`Add member successfully`);
// * 关闭弹窗
this.isInvateModalVisible = false;

View File

@ -2,8 +2,8 @@ import { Injectable } from '@angular/core';
import { EoNgFeedbackMessageService } from 'eo-ng-feedback';
import { autorun, toJS } from 'mobx';
import { ApiService } from 'pc/browser/src/app/services/storage/api.service';
import { EffectService } from 'pc/browser/src/app/store/effect.service';
import { StoreService } from 'pc/browser/src/app/store/state.service';
import { EffectService } from 'pc/browser/src/app/shared/store/effect.service';
import { StoreService } from 'pc/browser/src/app/shared/store/state.service';
import { Role, ROLE_TITLE_BY_ID } from '../../../../shared/models/member.model';

View File

@ -4,8 +4,8 @@ import { OperateProjectFormComponent } from 'pc/browser/src/app/pages/workspace/
import { ModalService } from 'pc/browser/src/app/services/modal.service';
import { ApiService } from 'pc/browser/src/app/services/storage/api.service';
import { EffectService } from '../../../../store/effect.service';
import { StoreService } from '../../../../store/state.service';
import { EffectService } from '../../../../shared/store/effect.service';
import { StoreService } from '../../../../shared/store/state.service';
import { ProjectListService } from './project-list.service';
@Component({

View File

@ -4,8 +4,8 @@ import { TraceService } from 'pc/browser/src/app/services/trace.service';
import { SettingService } from '../../../../components/system-setting/settings.service';
import { ModalService } from '../../../../services/modal.service';
import { EffectService } from '../../../../store/effect.service';
import { StoreService } from '../../../../store/state.service';
import { EffectService } from '../../../../shared/store/effect.service';
import { StoreService } from '../../../../shared/store/state.service';
import { OperateProjectFormComponent } from '../../project/components/operate-project-form.compoent';
type ListType = 'list' | 'card';
@Injectable({

View File

@ -6,7 +6,7 @@ import { MessageService } from 'pc/browser/src/app/services/message';
import { waitNextTick } from 'pc/browser/src/app/shared/utils/index.utils';
import { FeatureControlService } from '../../../core/services/feature-control/feature-control.service';
import { StoreService } from '../../../store/state.service';
import { StoreService } from '../../../shared/store/state.service';
import { ProjectListService } from './project-list/project-list.service';
@Component({
@ -22,7 +22,7 @@ export class WorkspaceOverviewComponent implements OnInit {
public projectList: ProjectListService,
public store: StoreService,
private router: Router,
private message: EoNgFeedbackMessageService,
private feedback: EoNgFeedbackMessageService,
public feature: FeatureControlService,
private postMessage: MessageService
) {}
@ -31,7 +31,7 @@ export class WorkspaceOverviewComponent implements OnInit {
this.router.navigate(['/home/workspace/overview/member']);
}
if (this.store.isLocal) {
this.message.info($localize`You should switch to cloud workspace and invite members.`);
this.feedback.info($localize`You should switch to cloud workspace and invite members.`);
return;
}
await waitNextTick();

View File

@ -33,9 +33,13 @@ const routes: Routes = [
path: 'test',
loadChildren: () => import('./http/test/api-test.module').then(m => m.ApiTestModule)
},
{
path: 'case',
loadChildren: () => import('./http/test/api-test.module').then(m => m.ApiTestModule)
},
{
path: 'mock',
loadChildren: () => import('./http/mock/api-mock.module').then(m => m.ApiMockModule)
loadChildren: () => import('./http/mock/mock.module').then(m => m.MockModule)
}
]
},
@ -54,7 +58,7 @@ const routes: Routes = [
},
{
path: 'group',
loadChildren: () => import('./group/group.module').then(m => m.GroupModule)
loadChildren: () => import('./group-edit/group.module').then(m => m.GroupModule)
}
// {
// path: 'grpc',

View File

@ -4,25 +4,32 @@ import { EoNgTreeModule } from 'eo-ng-tree';
import { NzEmptyModule } from 'ng-zorro-antd/empty';
import { EoMonacoEditorModule } from 'pc/browser/src/app/components/eo-ui/monaco-editor/monaco.module';
import { EoTableProModule } from 'pc/browser/src/app/components/eo-ui/table-pro/table-pro.module';
import { ActionComponent } from 'pc/browser/src/app/pages/workspace/project/api/components/action/action.component';
import { ApiMockTableComponent } from 'pc/browser/src/app/pages/workspace/project/api/components/api-mock-table.component';
import { ApiScriptComponent } from 'pc/browser/src/app/pages/workspace/project/api/components/api-script/api-script.component';
import { ApiTestFormComponent } from 'pc/browser/src/app/pages/workspace/project/api/components/api-test-form/api-test-form.component';
import { ApiTestResultHeaderComponent } from 'pc/browser/src/app/pages/workspace/project/api/components/api-test-result-header/api-test-result-header.component';
import { ParamsImportComponent } from 'pc/browser/src/app/pages/workspace/project/api/components/params-import/params-import.component';
import { ApiMockService } from 'pc/browser/src/app/pages/workspace/project/api/http/mock/api-mock.service';
import { ApiTestService } from 'pc/browser/src/app/pages/workspace/project/api/http/test/api-test.service';
import { ApiTableService } from 'pc/browser/src/app/pages/workspace/project/api/service/api-table.service';
import { SharedModule } from 'pc/browser/src/app/shared/shared.module';
import { ApiFormaterPipe } from './pipe/api-formater.pipe';
import { ApiParamsNumPipe } from './pipe/api-param-num.pipe';
const COMPONENTS = [ApiTestFormComponent, ParamsImportComponent, ApiTestResultHeaderComponent, ApiMockTableComponent];
const COMPONENTS = [
ApiTestFormComponent,
ApiScriptComponent,
ActionComponent,
ParamsImportComponent,
ApiTestResultHeaderComponent,
ApiMockTableComponent
];
const SHARE_UI = [EoTableProModule, EoNgTabsModule];
const SHARE_PIPE = [ApiFormaterPipe, ApiParamsNumPipe];
@NgModule({
imports: [SharedModule, EoMonacoEditorModule, EoNgTreeModule, NzEmptyModule, ...SHARE_UI],
declarations: [...COMPONENTS, ...SHARE_PIPE],
providers: [ApiTableService, ApiTestService, ApiMockService],
providers: [ApiTableService],
exports: [...COMPONENTS, ...SHARE_PIPE, ...SHARE_UI]
})
export class ApiSharedModule {}

View File

@ -1,135 +1,173 @@
import { Injectable } from '@angular/core';
import { Inject, Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { autorun } from 'mobx';
import { TabItem } from 'pc/browser/src/app/components/eo-ui/tab/tab.model';
import { requestMethodMap } from 'pc/browser/src/app/pages/workspace/project/api/api.model';
import { autorun, reaction, toJS } from 'mobx';
import { EditTabViewComponent, TabItem } from 'pc/browser/src/app/components/eo-ui/tab/tab.model';
import { BASIC_TABS_INFO, requestMethodMap, TabsConfig } from 'pc/browser/src/app/pages/workspace/project/api/constants/api.model';
import { ApiStoreService } from 'pc/browser/src/app/pages/workspace/project/api/store/api-state.service';
import { Message } from 'pc/browser/src/app/services/message';
import { StoreService } from 'pc/browser/src/app/store/state.service';
import { GroupModuleType, GroupType, ViewGroup } from 'pc/browser/src/app/services/storage/db/models';
import { StoreService } from 'pc/browser/src/app/shared/store/state.service';
import { flatTree } from 'pc/browser/src/app/shared/utils/tree/tree.utils';
import { debounceTime, Subject } from 'rxjs';
import { EoTabComponent } from '../../../../components/eo-ui/tab/tab.component';
import { MessageService } from '../../../../services/message';
import { isEmptyObj } from '../../../../shared/utils/index.utils';
import { eoDeepCopy as pcDeepCopy, isEmptyObj } from '../../../../shared/utils/index.utils';
export enum PageUniqueName {
HttpTest = 'api-http-test',
HttpDetail = 'api-http-detail',
HttpEdit = 'api-http-edit',
HttpCase = 'api-http-case-edit',
HttpMock = 'api-http-mock-edit',
WsTest = 'api-ws-test',
EnvEdit = 'project-env-edit',
GroupEdit = 'project-group'
}
export const API_TABS: Array<Partial<TabItem>> = [
{
pathname: '/http/test',
uniqueName: PageUniqueName.HttpTest,
type: 'edit',
title: $localize`New Request`,
extends: { method: 'POST' }
},
{
pathname: '/env/edit',
uniqueName: PageUniqueName.EnvEdit,
type: 'edit',
icon: 'application',
title: $localize`New Environment`
},
{
pathname: '/group/edit',
uniqueName: PageUniqueName.GroupEdit,
type: 'edit',
icon: 'folder-close',
title: $localize`:@@AddGroup:New Group`
},
{
pathname: '/http/edit',
uniqueName: PageUniqueName.HttpEdit,
isFixed: true,
type: 'edit',
title: $localize`New API`
},
{ pathname: '/http/detail', uniqueName: PageUniqueName.HttpDetail, type: 'preview', title: $localize`Preview` },
{
pathname: '/ws/test',
uniqueName: PageUniqueName.WsTest,
isFixed: true,
type: 'edit',
extends: { method: 'WS' },
title: $localize`New Websocket`
},
{ pathname: '/http/case', icon: 'diy-test', uniqueName: PageUniqueName.HttpCase, type: 'edit', title: $localize`New Case` },
{ pathname: '/http/mock', icon: 'mock', uniqueName: PageUniqueName.HttpMock, type: 'edit', title: $localize`New Mock` }
];
interface TabEvent {
when: 'activated' | 'editing' | 'saved' | 'afterTested';
currentTabID: TabItem['uuid'];
model?: any;
}
@Injectable()
export class ApiTabService {
componentRef;
componentRef: EditTabViewComponent | any;
apiTabComponent: EoTabComponent;
// Set current tab type:'preview'|'edit' for later judgment
get currentComponentTab(): Partial<TabItem> {
return this.BASIC_TABS.find(val => this.router.url.includes(val.pathname));
}
private changeContent$: Subject<any> = new Subject();
SHARE_TABS: Array<Partial<TabItem>> = [
{
pathname: '/share/http/test',
id: 'share-api-test',
type: 'edit',
title: $localize`New Request`,
extends: { method: 'POST' }
},
{ pathname: '/share/http/detail', id: 'share-api-detail', type: 'preview', title: $localize`Preview` },
{ pathname: '/share/group/edit', id: 'share-group-edit', type: 'preview', title: $localize`Preview` },
{
pathname: '/share/ws/test',
id: 'share-api-test',
isFixed: true,
type: 'preview',
extends: { method: 'WS' },
title: $localize`New Websocket`
}
];
API_TABS: Array<Partial<TabItem>> = [
{
pathname: '/home/workspace/project/api/http/test',
id: 'api-http-test',
type: 'edit',
title: $localize`New Request`,
extends: { method: 'POST' }
},
{
pathname: '/home/workspace/project/api/env/edit',
id: 'project-env',
type: 'edit',
icon: 'application',
title: $localize`New Environment`
},
{
pathname: '/home/workspace/project/api/group/edit',
id: 'project-group',
type: 'edit',
icon: 'folder-close',
title: $localize`:@@AddGroup:New Group`
},
{ pathname: '/home/workspace/project/api/http/edit', id: 'api-http-edit', isFixed: true, type: 'edit', title: $localize`New API` },
{ pathname: '/home/workspace/project/api/http/detail', id: 'api-http-detail', type: 'preview', title: $localize`Preview` },
{
pathname: '/home/workspace/project/api/ws/test',
id: 'api-ws-test',
isFixed: true,
type: 'edit',
extends: { method: 'WS' },
title: $localize`New Websocket`
},
{ pathname: '/home/workspace/project/api/http/mock', id: 'api-http-mock', type: 'preview', title: 'Mock' }
];
private changeContent$: Subject<TabEvent> = new Subject();
BASIC_TABS: Array<Partial<TabItem>>;
constructor(private messageService: MessageService, private router: Router, private store: StoreService) {
constructor(
private messageService: MessageService,
private router: Router,
private globalStore: StoreService,
private store: ApiStoreService,
@Inject(BASIC_TABS_INFO) public tabsConfig: TabsConfig
) {
this.changeContent$.pipe(debounceTime(150)).subscribe(inData => {
this.afterContentChanged(inData);
this.afterTabContentChanged(inData);
});
this.messageService.get().subscribe((inArg: Message) => {
this.watchApiChange(inArg);
});
autorun(() => {
this.BASIC_TABS = this.store.isShare ? this.SHARE_TABS : this.API_TABS;
if (inArg.type !== 'tabContentInit') return;
this.updateTabContent(inArg.data.uuid);
});
this.BASIC_TABS = this.tabsConfig.BASIC_TABS;
this.closeTabAfterResourceRemove();
}
watchApiChange(inArg: Message) {
switch (inArg.type) {
case 'deleteApiSuccess': {
//Close those tab who has been deleted
/**
* Watch API/Group/Case/Env/Mock change for handle tab status to fit content
*
* It is optimal to control Tab closing through a specific event transmission ID, but this event will always be ignored in use
*
* @param inArg
*/
closeTabAfterResourceRemove() {
const checkTabIsExist = (groups: ViewGroup[], tab: TabItem) => {
const isExist = groups.some(group => {
//TODO check group.id is same as resource id
if (!tab.params?.uuid) return true;
const modelID = Number(tab.params.uuid) || tab.params.uuid;
if (modelID === group.id && group.type === GroupType.UserCreated && tab.uniqueName === PageUniqueName.GroupEdit) {
return true;
}
if (
modelID === group.relationInfo?.apiUuid &&
group.module === GroupModuleType.API &&
[PageUniqueName.HttpEdit, PageUniqueName.HttpDetail, PageUniqueName.HttpTest].includes(tab.uniqueName as PageUniqueName)
) {
return true;
}
if (
modelID === group.relationInfo?.apiCaseUuid &&
group.module === GroupModuleType.Case &&
tab.uniqueName === PageUniqueName.HttpCase
) {
return true;
}
if (modelID === group.relationInfo?.id && group.module === GroupModuleType.Mock && tab.uniqueName === PageUniqueName.HttpMock) {
return true;
}
return false;
});
return isExist;
};
//Delete group/api/case/mock
reaction(
() => this.store.getGroupList,
(value, previousValue) => {
const currentFlatTree = flatTree(value);
const previousFlatTres = flatTree(previousValue);
const hasDeleted = currentFlatTree.length < previousFlatTres.length;
if (!hasDeleted) return;
//Close them
const closeTabIDs = this.apiTabComponent
.getTabs()
.filter((val: TabItem) => val.pathname.includes('home/workspace/project/api/http') && inArg.data.uuids.includes(val.params.uuid))
.filter((tab: TabItem) => !checkTabIsExist(currentFlatTree, tab))
.map(val => val.uuid);
this.apiTabComponent.batchCloseTab(closeTabIDs);
break;
}
case 'deleteEnvSuccess': {
);
//Delete env
reaction(
() => this.store.getEnvList,
(value, previousValue) => {
const hasDeleted = value.length < previousValue.length;
if (!hasDeleted) return;
const closeTabIDs = this.apiTabComponent
.getTabs()
.filter(
(val: TabItem) =>
val.pathname.includes('home/workspace/project/api/env/edit') && inArg.data.uuids.includes(Number(val.params.uuid))
(tab: TabItem) =>
tab.params?.uuid && value.every(env => env.id.toString() !== tab.params.uuid) && tab.uniqueName === PageUniqueName.EnvEdit
)
.map(val => val.uuid);
this.apiTabComponent.batchCloseTab(closeTabIDs);
break;
}
case 'deleteGroupSuccess': {
const closeTabIDs = this.apiTabComponent
.getTabs()
.filter(
(val: TabItem) =>
val.pathname.includes('home/workspace/project/api/group/edit') && inArg.data.uuids.includes(Number(val.params.uuid))
)
.map(val => val.uuid);
this.apiTabComponent.batchCloseTab(closeTabIDs);
break;
}
case 'tabContentInit': {
this.updateChildView(this.router.url);
break;
}
}
}
batchCloseTabById(uuidList) {
const result = this.apiTabComponent
.getTabs()
.filter(it => uuidList.includes(it.params.uuid))
.map(it => it.uuid);
this.apiTabComponent.batchCloseTab(result);
);
}
onChildComponentInit(componentRef) {
this.componentRef = componentRef;
@ -138,35 +176,87 @@ export class ApiTabService {
* After tab component/child component init
*/
onAllComponentInit() {
const url = this.router.url;
this.updateChildView(url);
//We need to wait for tabComponent and childComponent onInit finished
this.updateTabContent();
}
private bindChildComponentChangeEvent() {
if (!this.componentRef) {
return;
}
const url = this.router.url;
//Bind event tab
const bindTabID = this.apiTabComponent.getCurrentTab()?.uuid;
this.componentRef.eoOnInit = {
emit: model => {
this.afterContentChanged({ when: 'init', url, model });
//Current is current selected tab
const currentTab = this.apiTabComponent.getCurrentTab();
if (!model) {
pcConsole.warn('[api-tab] eoOnInit cannot pass in null value, this tab will be closed automatically');
this.apiTabComponent.batchCloseTab([currentTab.uuid]);
return;
}
//resourceID
let modelID: number;
switch (currentTab.uniqueName) {
case PageUniqueName.HttpEdit:
case PageUniqueName.HttpTest:
case PageUniqueName.HttpEdit: {
modelID = model.apiUuid;
break;
}
default: {
modelID = model.uuid || model.id;
break;
}
}
//1. The currently active tab is not the one that initiated the request
const notCurrentTab = currentTab.uuid !== bindTabID;
//2. the request response is not what the current active tab needs
const notCurrentResource = modelID && currentTab.params?.uuid && currentTab.params?.uuid !== modelID.toString();
const modelFromOtherTab = notCurrentTab || notCurrentResource;
if (!modelFromOtherTab) {
this.afterTabContentChanged({ when: 'activated', currentTabID: bindTabID, model });
return;
}
//! The previous Tab's(bindTab) request may overwrite the current Tab's(currentTab) data.
pcConsole.warn(
`The current tab data will be restored from the cache to prevent it from being overwritten by the result of the previous Tab asynchronous request.
previous tab:${model.name}
current tab:${currentTab.title}`
);
//* When data inconsistent, we need to manually reset the model from cache
const hasCache = !!currentTab?.content?.[currentTab.uniqueName];
if (!currentTab.isLoading && hasCache) {
//If the current tab is not the one that initiated the request, we need to restore the data from the cache
this.afterTabActivated(currentTab);
}
const actuallyID = this.apiTabComponent.getTabByParamsID(modelID.toString())?.uuid || bindTabID;
this.afterTabContentChanged({ when: 'activated', currentTabID: actuallyID, model });
}
};
//Edit page has save/editing event
if (this.currentComponentTab.type === 'edit') {
this.componentRef.afterSaved = {
emit: model => {
this.afterContentChanged({ when: 'saved', url, model });
this.afterTabContentChanged({ when: 'saved', currentTabID: bindTabID, model });
}
};
this.componentRef.modelChange = {
emit: model => {
this.changeContent$.next({ when: 'editing', url, model });
this.changeContent$.next({ when: 'editing', currentTabID: bindTabID, model });
}
};
}
//Test page has tested event
if (this.currentComponentTab.pathname.includes('test')) {
this.componentRef.afterTested = {
emit: model => {
this.afterContentChanged({ when: 'afterTested', url, model });
this.afterTabContentChanged({ when: 'afterTested', currentTabID: bindTabID, model });
}
};
}
@ -180,131 +270,188 @@ export class ApiTabService {
if (!needSave) {
return;
}
this.componentRef.saveApi();
this.componentRef.beforeTabClose();
}
getCurrentTabCache(currentTab: TabItem) {
const contentID = currentTab.uniqueName;
//Get tab from cache
return {
hasCache: !!currentTab?.content?.[contentID],
model: currentTab?.content?.[contentID] || null,
initialModel: currentTab?.baseContent?.[contentID] || null
};
}
/**
* Reflesh data after Tab init
* Call tab component afterTabActivated
*
* @param currentTab
*/
afterTabActivated(currentTab: TabItem) {
const cacheResult = this.getCurrentTabCache(currentTab);
this.componentRef.model = cacheResult.model;
this.componentRef.initialModel = cacheResult.initialModel;
this.componentRef.afterTabActivated();
}
/**
* Reflesh tab content[childComponent] after Tab activated
*
* @param lastRouter
* @param currentRouter
* @returns
*/
updateChildView(url) {
updateTabContent(uuid?) {
if (!this.apiTabComponent) {
return;
}
this.bindChildComponentChangeEvent();
if (!this.componentRef?.init) {
this.changeContent$.next({ when: 'init', url });
pcConsole.error(
'Child componentRef need has init function for reflesh data when router change,Please add init function in child component'
);
return;
}
//?Why should use getCurrentTab()?
//?Why should use getCurrentTab() directly
//Because maybe current tab has't finish init
const currentTab = this.apiTabComponent.getExistTabByUrl(url);
const currentTab = uuid ? this.apiTabComponent.getTabByID(uuid) : this.apiTabComponent.getCurrentTab();
if (!currentTab) {
return;
}
const contentID = currentTab.id;
if (!this.componentRef?.afterTabActivated) {
this.changeContent$.next({ when: 'activated', currentTabID: currentTab.uuid });
pcConsole.error(
'Child componentRef need has afterTabActivated function for reflesh data when router change,Please add afterTabActivated function in child component'
);
return;
}
this.afterTabActivated(currentTab);
}
/**
* Generate tab header info,title,method,icon and so on
*
* @param currentTab
* @param model
* @returns
*/
getTabHeaderInfo(currentTab, model): { title: string; method: string } {
const result = {
title: model.name,
method: ''
};
result.title = model.name;
result.method = requestMethodMap[model.apiAttrInfo?.requestMethod];
//Get tab from cache
if (!currentTab.disabledCache) {
this.componentRef.model = currentTab?.content?.[contentID] || null;
this.componentRef.initialModel = currentTab?.baseContent?.[contentID] || null;
} else {
this.componentRef.model = null;
this.componentRef.initialModel = null;
const isTestPage = [PageUniqueName.HttpCase, PageUniqueName.HttpTest, PageUniqueName.WsTest].includes(currentTab.uniqueName);
const isEmptyPage = !model.uuid;
if (!isTestPage) {
if (isEmptyPage) {
result.title = result.title || this.BASIC_TABS.find(val => val.pathname === currentTab.pathname).title;
}
return result;
}
this.componentRef.init();
//Test page,generate title and method from model.url
switch (currentTab.uniqueName) {
case PageUniqueName.WsTest: {
result.method = 'WS';
break;
}
case PageUniqueName.HttpTest: {
result.method = requestMethodMap[model.request.apiAttrInfo?.requestMethod];
break;
}
}
//Only Untitle request need set url to tab title
const originTitle = this.BASIC_TABS.find(val => val.pathname === currentTab.pathname)?.title;
const isHistoryPage = currentTab.params?.uuid?.includes('history_');
if (!model.request.uuid || isHistoryPage) {
result.title = model.request.uri || originTitle;
} else {
result.title = model.request.name || originTitle;
}
return result;
}
updateTab(currentTab, inData) {
updateTab(currentTab: TabItem, inData: TabEvent) {
const model = inData.model;
const contentID = currentTab.id;
if (!model || isEmptyObj(model)) return;
const contentID = currentTab.uniqueName;
// if (!currentTab.baseContent) {
// console.error('nononononnononononnononononnononononnononononnononononnonononon baseContent lose', inData.when, currentTab.uuid);
// }
//Set tabItem
const replaceTab: Partial<TabItem> = {
hasChanged: currentTab.hasChanged,
isLoading: false,
extends: {}
};
if (model && !isEmptyObj(model)) {
//Set title/method
replaceTab.title = model.name;
replaceTab.extends.method = requestMethodMap[model.apiAttrInfo?.requestMethod];
if (currentTab.pathname.includes('test')) {
if (currentTab.pathname === '/home/workspace/project/api/ws/test') {
replaceTab.extends.method = 'WS';
} else {
replaceTab.extends.method = requestMethodMap[model.request.apiAttrInfo?.requestMethod];
}
//Only Untitle request need set url to tab title
const originTitle = this.BASIC_TABS.find(val => val.pathname === currentTab.pathname)?.title;
if (!model.request.uuid || (currentTab.params.uuid && currentTab.params.uuid.includes('history_'))) {
replaceTab.title = model.request.uri || originTitle;
} else {
replaceTab.title = model.request.name || originTitle;
}
} else if (!model.uuid) {
replaceTab.title = replaceTab.title || this.BASIC_TABS.find(val => val.pathname === currentTab.pathname).title;
}
//Only hasChanged edit page storage data
if (currentTab.type === 'edit') {
let currentHasChanged = currentTab.extends?.hasChanged?.[contentID] || false;
switch (inData.when) {
case 'editing': {
// Saved APIs do not need to verify changes
if (currentTab.module !== 'test' || !currentTab.params.uuid || currentTab.params.uuid.includes('history')) {
//Set hasChange
if (!this.componentRef?.isFormChange) {
throw new Error(
`EO_ERROR:Child componentRef[${this.componentRef.constructor.name}] need has isFormChange function check model change`
);
}
currentHasChanged = this.componentRef.isFormChange();
} else {
currentHasChanged = false;
}
break;
}
case 'saved': {
currentHasChanged = false;
}
}
//* Share change status within all content page
replaceTab.extends.hasChanged = currentTab.extends?.hasChanged || {};
replaceTab.extends.hasChanged[contentID] = currentHasChanged;
// Editiable tab share hasChanged data
if (!currentHasChanged && currentTab.extends?.hasChanged) {
const otherEditableTabs = this.BASIC_TABS.filter(val => val.type === 'edit' && val.id !== contentID);
currentHasChanged = otherEditableTabs.some(tabItem => currentTab.extends?.hasChanged[tabItem.id]);
}
replaceTab.hasChanged = currentHasChanged;
// Set storage
//Set baseContent
if (['init', 'saved'].includes(inData.when)) {
const initialModel = this.componentRef.initialModel;
replaceTab.baseContent = inData.when === 'saved' ? {} : currentTab.baseContent || {};
replaceTab.baseContent[contentID] = initialModel && !isEmptyObj(initialModel) ? initialModel : null;
}
//Set content
replaceTab.content = inData.when === 'saved' ? {} : currentTab.content || {};
replaceTab.content[contentID] = model && !isEmptyObj(model) ? model : null;
}
//* Set title/method
const tabHeaderInfo = this.getTabHeaderInfo(currentTab, model);
replaceTab.title = tabHeaderInfo.title;
replaceTab.extends.method = tabHeaderInfo.method;
//Set isFixed
if (replaceTab.hasChanged) {
replaceTab.isFixed = true;
//* Set Edit page,such as tab title,storage data,unsaved status by check model change
if (currentTab.type === 'edit') {
//Set tab storage
//Set baseContent
if (['activated', 'saved'].includes(inData.when)) {
const initialModel = pcDeepCopy(inData.model);
//Update tab by id,may not be the current selected tab
const isCurrentSelectedTab = currentTab.uuid === this.apiTabComponent.getCurrentTab().uuid;
//If is current tab,set initialModel automatically
if (isCurrentSelectedTab) {
this.componentRef.initialModel = initialModel;
}
//Saved data may update all IntialData
replaceTab.baseContent = inData.when === 'saved' ? {} : currentTab.baseContent || {};
replaceTab.baseContent[contentID] = initialModel && !isEmptyObj(initialModel) ? initialModel : null;
}
//Has tested/exsix api set fixed
if (currentTab.pathname.includes('test') && (model.testStartTime !== undefined || currentTab.params.uuid)) {
replaceTab.isFixed = true;
//Set content
replaceTab.content = inData.when === 'saved' ? {} : currentTab.content || {};
replaceTab.content[contentID] = model && !isEmptyObj(model) ? model : null;
let currentHasChanged = currentTab.extends?.hasChanged?.[contentID];
switch (inData.when) {
case 'editing': {
//Set hasChange
if (!this.componentRef?.isFormChange) {
throw new Error(
`EO_ERROR:Child componentRef[${this.componentRef.constructor.name}] need has isFormChange function check model change`
);
}
currentHasChanged = this.componentRef.isFormChange();
break;
}
case 'saved': {
currentHasChanged = false;
break;
}
}
//* Share change status within all content page
replaceTab.extends.hasChanged = currentTab.extends?.hasChanged || {};
replaceTab.extends.hasChanged[contentID] = currentHasChanged;
// Editiable tab share hasChanged data
if (!currentHasChanged && currentTab.extends?.hasChanged) {
const otherEditableTabs = this.BASIC_TABS.filter(val => val.type === 'edit' && val.uniqueName !== contentID);
currentHasChanged = otherEditableTabs.some(tabItem => currentTab.extends?.hasChanged[tabItem.uniqueName]);
}
replaceTab.hasChanged = currentHasChanged;
}
this.apiTabComponent.updatePartialTab(inData.url, replaceTab);
//Set isFixed
if (replaceTab.hasChanged) {
replaceTab.isFixed = true;
}
//Has tested/exsix api set fixed
const isTestPage = [PageUniqueName.HttpCase, PageUniqueName.HttpTest, PageUniqueName.WsTest].includes(
currentTab.uniqueName as PageUniqueName
);
if (isTestPage && model.testStartTime !== undefined) {
replaceTab.isFixed = true;
}
// console.log('updatePartialTab', currentTab.uuid, replaceTab);
this.apiTabComponent.updatePartialTab(currentTab.uuid, replaceTab);
}
/**
* After content changed
@ -312,32 +459,44 @@ export class ApiTabService {
*
* @param inData.url get component fit tab data
*/
afterContentChanged(inData: { when: 'init' | 'editing' | 'saved' | 'afterTested'; url: string; model: any }) {
afterTabContentChanged(inData: TabEvent) {
if (!this.apiTabComponent) {
pcConsole.warn(`ING[api-tab]: apiTabComponent hasn't init yet!`);
return;
}
let currentTab = this.apiTabComponent.getExistTabByUrl(inData.url);
const currentTab = this.apiTabComponent.getTabByID(inData.currentTabID);
if (!currentTab) {
pcConsole.warn(`ING[api-tab]: has't find the tab fit child component ,url:${inData.url}`);
pcConsole.warn(`ING[api-tab]: has't find the tab fit child component ,url:${inData.currentTabID}`);
return;
}
//Unit request is asynchronous,Update other tab test result
if (inData?.when === 'afterTested') {
//Update other tab test result
inData.url = `${inData.model.url}?pageID=${inData.model.id}`;
currentTab = this.apiTabComponent.getExistTabByUrl(inData.url);
inData.model = { ...currentTab.content.test, ...inData.model.model };
}
this.updateTab(currentTab, inData);
}
/**
* Handle cache data before restore tab info
*
* @param tabsInfo
* @returns
*/
handleDataBeforeGetCache = tabsInfo => {
if (!tabsInfo?.tabOrder?.[0]) return null;
const tab = tabsInfo.tabsByID[tabsInfo.tabOrder[0]];
if (!tab) return null;
const { wid, pid } = tab.params;
if (wid !== this.store.getCurrentWorkspaceUuid || pid !== this.store.getCurrentProjectID) return null;
if (wid !== this.globalStore.getCurrentWorkspaceUuid || pid !== this.globalStore.getCurrentProjectID) return null;
return tabsInfo;
};
/**
* Handle cache data before storage tab info
*
* @param tabsInfo
* @returns
*/
handleDataBeforeCache = tabStorage => {
Object.values(tabStorage.tabsByID).forEach((val: TabItem) => {
//Delete gio key

View File

@ -1,21 +1,19 @@
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Component, Inject, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { NzResizeEvent } from 'ng-zorro-antd/resizable';
import { EoTabComponent } from 'pc/browser/src/app/components/eo-ui/tab/tab.component';
import { WebService } from 'pc/browser/src/app/core/services';
import { BASIC_TABS_INFO, TabsConfig } from 'pc/browser/src/app/pages/workspace/project/api/constants/api.model';
import { ExtensionService } from 'pc/browser/src/app/services/extensions/extension.service';
import { ApiData } from 'pc/browser/src/app/services/storage/index.model';
import { TraceService } from 'pc/browser/src/app/services/trace.service';
import { API_PREVIEW_TAB } from 'pc/browser/src/app/shared/constans/featureName';
import { ExtensionChange } from 'pc/browser/src/app/shared/decorators';
import { StoreService } from 'pc/browser/src/app/store/state.service';
import { StoreService } from 'pc/browser/src/app/shared/store/state.service';
import { filter, Subject, takeUntil } from 'rxjs';
import { SidebarService } from '../../../../layouts/sidebar/sidebar.service';
import { Message, MessageService } from '../../../../services/message';
import { ExtensionInfo } from '../../../../shared/models/extension-manager';
import StorageUtil from '../../../../shared/utils/storage/storage.utils';
import { ApiTabService } from './api-tab.service';
import { ApiTabService, PageUniqueName } from './api-tab.service';
const RIGHT_SIDER_WIDTH_KEY = 'RIGHT_SIDER_WIDTH';
const LEFT_SIDER_WIDTH_KEY = 'LEFT_SIDER_WIDTH_KEY';
@ -58,27 +56,21 @@ export class ApiComponent implements OnInit, OnDestroy {
routerLink: 'detail',
isShare: true,
//ID fit to the routerLink
id: 'api-http-detail',
id: PageUniqueName.HttpDetail,
title: $localize`:@@API Detail:Preview`
},
{
routerLink: 'edit',
id: 'api-http-edit',
id: PageUniqueName.HttpEdit,
title: $localize`Edit`
},
{
routerLink: 'test',
isShare: true,
id: 'api-http-test',
id: PageUniqueName.HttpTest,
title: $localize`Test`
},
{
routerLink: 'mock',
id: 'api-http-mock',
title: 'Mock'
}
];
originModel: ApiData | any;
rightSiderWidth = this.getLocalRightSiderWidth();
tabsIndex = StorageUtil.get('eo_group_tab_select') || 0;
@ -90,17 +82,17 @@ export class ApiComponent implements OnInit, OnDestroy {
public sidebar: SidebarService,
private router: Router,
public web: WebService,
private messageService: MessageService,
private extensionService: ExtensionService,
public store: StoreService,
private trace: TraceService
private trace: TraceService,
@Inject(BASIC_TABS_INFO) public tabsConfig: TabsConfig
) {
this.initExtensionExtra();
}
@ExtensionChange(API_PREVIEW_TAB, true)
async initExtensionExtra() {
this.rightExtras = [];
if (!this.router.url.includes('home/workspace/project/api/http/detail')) return;
if (!this.router.url.includes(this.tabsConfig.pathByName[PageUniqueName.HttpDetail])) return;
const apiPreviewTab = this.extensionService.getValidExtensionsByFature(API_PREVIEW_TAB);
apiPreviewTab?.forEach(async (value, key) => {
const module = await this.extensionService.getExtensionPackage(key);
@ -131,10 +123,14 @@ export class ApiComponent implements OnInit, OnDestroy {
this.apiTab.onChildComponentInit(componentRef);
}
initChildBarShowStatus() {
const isEnvPage = this.router.url.includes('home/workspace/project/api/env/edit');
const isGroupPage = ['share/group/edit', 'home/workspace/project/api/group/edit'].some(n => this.router.url.includes(n));
const pathArr = [
this.tabsConfig.pathByName[PageUniqueName.HttpDetail],
this.tabsConfig.pathByName[PageUniqueName.HttpEdit],
this.tabsConfig.pathByName[PageUniqueName.HttpTest]
];
const isApiPage = pathArr.some(path => this.router.url.includes(path));
const isTestHistoryPage = this.route.snapshot.queryParams.uuid?.includes('history_');
this.showChildBar = this.route.snapshot.queryParams.uuid && !isTestHistoryPage && !isEnvPage && !isGroupPage;
this.showChildBar = this.route.snapshot.queryParams.uuid && !isTestHistoryPage && isApiPage;
}
onGroupTabSelectChange($event) {
StorageUtil.set('eo_group_tab_select', this.tabsIndex);

View File

@ -8,22 +8,25 @@ import { EoNgTreeModule } from 'eo-ng-tree';
import { NzBadgeModule } from 'ng-zorro-antd/badge';
import { NzEmptyModule } from 'ng-zorro-antd/empty';
import { NzResizableModule, NzResizableService } from 'ng-zorro-antd/resizable';
import { ApiTabService } from 'pc/browser/src/app/pages/workspace/project/api/api-tab.service';
import { ApiGroupTreeDirective } from 'pc/browser/src/app/pages/workspace/project/api/components/group/tree/api-group-tree.directive';
import { ApiTabService, API_TABS } from 'pc/browser/src/app/pages/workspace/project/api/api-tab.service';
import { ApiGroupTreeDirective } from 'pc/browser/src/app/pages/workspace/project/api/components/group/api-group-tree.directive';
import { ResponseStepsComponent } from 'pc/browser/src/app/pages/workspace/project/api/components/response-steps/response-steps.component';
import { BASIC_TABS_INFO, TabsConfig } from 'pc/browser/src/app/pages/workspace/project/api/constants/api.model';
import { ApiMockService } from 'pc/browser/src/app/pages/workspace/project/api/http/mock/api-mock.service';
import { ApiCaseService } from 'pc/browser/src/app/pages/workspace/project/api/http/test/api-case.service';
import { SharedModule } from 'pc/browser/src/app/shared/shared.module';
import { EoTabModule } from '../../../../components/eo-ui/tab/tab.module';
import { ExtensionSelectModule } from '../../../../components/extension-select/extension-select.module';
import { ApiRoutingModule } from './api-routing.module';
import { ApiComponent } from './api.component';
import { ProjectApiService } from './api.service';
import { ApiGroupEditComponent } from './components/group/edit/api-group-edit.component';
import { ApiGroupTreeComponent } from './components/group/tree/api-group-tree.component';
import { ApiGroupTreeComponent } from './components/group/api-group-tree.component';
import { HistoryComponent } from './components/history/eo-history.component';
import { EnvModule } from './env/env.module';
import { ApiTestUtilService } from './service/api-test-util.service';
const COMPONENTS = [ApiComponent, ApiGroupEditComponent, ApiGroupTreeComponent, HistoryComponent];
import { ProjectApiService } from './service/project-api.service';
const COMPONENTS = [ApiComponent, ApiGroupTreeComponent, HistoryComponent];
const tabs = API_TABS.map(val => ({ ...val, pathname: `/home/workspace/project/api${val.pathname}` }));
@NgModule({
imports: [
ExtensionSelectModule,
@ -39,10 +42,25 @@ const COMPONENTS = [ApiComponent, ApiGroupEditComponent, ApiGroupTreeComponent,
NzBadgeModule,
EoNgLayoutModule,
EoNgTabsModule,
EoNgTreeModule
EoNgTreeModule,
ResponseStepsComponent
],
declarations: [...COMPONENTS, ApiGroupTreeDirective],
exports: [ApiComponent],
providers: [ProjectApiService, ApiTestUtilService, NzResizableService, ApiTabService]
providers: [
{
provide: BASIC_TABS_INFO,
useValue: {
BASIC_TABS: tabs,
pathByName: tabs.reduce((acc, curr) => ({ ...acc, [curr.uniqueName]: curr.pathname }), {})
} as TabsConfig
},
ApiCaseService,
ProjectApiService,
ApiTestUtilService,
NzResizableService,
ApiTabService,
ApiMockService
]
})
export class ApiModule {}

View File

@ -1,80 +0,0 @@
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { EoNgFeedbackMessageService } from 'eo-ng-feedback';
import { MessageService } from '../../../../services/message';
import { ApiService } from '../../../../services/storage/api.service';
import { ApiData } from '../../../../services/storage/db/models/apiData';
import { StoreService } from '../../../../store/state.service';
import { ApiEffectService } from './store/api-effect.service';
@Injectable()
export class ProjectApiService {
constructor(
private message: EoNgFeedbackMessageService,
private messageService: MessageService,
private router: Router,
private effect: ApiEffectService,
private api: ApiService,
private globalStore: StoreService
) {}
async get(uuid): Promise<ApiData> {
const [result, err] = await (this.globalStore.isShare
? this.api.api_shareApiDataDetail({ apiUuids: [uuid], withParams: 1, sharedUuid: this.globalStore.getShareID })
: this.api.api_apiDataDetail({ apiUuids: [uuid], withParams: 1 }));
if (err || !result?.[0]) {
this.message.error($localize`Can't find this API`);
return;
}
const apiData = result[0];
apiData.apiAttrInfo ??= {};
apiData.responseList ??= [
{
responseParams: {
headerParams: [],
bodyParams: []
}
}
];
apiData.responseList[0].responseParams ??= {
responseParams: {
headerParams: [],
bodyParams: []
}
};
return apiData;
}
async edit(apiData: ApiData) {
return await this.api.api_apiDataUpdate({ api: apiData });
}
async add(apiData: ApiData) {
return await this.api.api_apiDataCreate({ apiList: [].concat([apiData]) });
}
async copy(apiID: string) {
const { apiUuid, id, ...apiData } = await this.get(apiID);
apiData.name += ' Copy';
const [result, err] = await this.add(apiData);
if (err) {
console.log(err);
this.message.error($localize`Copy API failed`);
return;
}
this.router.navigate(['/home/workspace/project/api/http/edit'], {
queryParams: { pageID: Date.now(), uuid: result[0].apiUuid }
});
this.effect.getGroupList();
}
async delete(apiUuid) {
// * delete API
const [, err] = await this.api.api_apiDataDelete({
apiUuids: [apiUuid]
});
if (err) {
this.message.error($localize`Delete API failed`);
return;
}
this.messageService.send({ type: 'deleteApiSuccess', data: { uuids: [apiUuid] } });
this.message.success($localize`Deleted API Successfully`);
this.effect.getGroupList();
}
}

View File

@ -0,0 +1,13 @@
<div class="action-contain h-[100%]">
<eo-ng-tabset [(nzSelectedIndex)]="selectedIndex" nzTabPosition="left" class="h-[100%]">
<eo-ng-tab *ngFor="let item of operationArr" [nzTitle]="item.title" class="h-[100%]">
<ng-container *ngIf="item.type === 'pre'">
<ng-content select="[name=pre]"></ng-content>
</ng-container>
<ng-container *ngIf="item.type === 'after'">
<ng-content select="[name=after]"></ng-content>
</ng-container>
</eo-ng-tab>
</eo-ng-tabset>
</div>

View File

@ -0,0 +1,28 @@
:host ::ng-deep .action-contain {
.ant-tabs-tabpane {
padding-left: unset !important;
border-left: 1px solid var(--border-color);
}
.ant-tabs-content-holder {
height: 100% !important;
}
.ant-tabs-tab {
text-align: left !important;
margin: 5px !important;
border: 1px solid transparent;
padding: 5px 20px !important;
}
.ant-tabs-tab:hover,
.ant-tabs-tab-active {
border: 1px solid var(--border-color);
background: var(--bar-background-color);
border-radius: 3px;
}
.ant-tabs > .ant-tabs-nav .ant-tabs-nav-list {
align-items: unset;
}
}

View File

@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ActionComponent } from './action.component';
describe('ActionComponent', () => {
let component: ActionComponent;
let fixture: ComponentFixture<ActionComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ActionComponent]
}).compileComponents();
fixture = TestBed.createComponent(ActionComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,31 @@
import { Component } from '@angular/core';
interface OperationType {
title: string;
type: string;
}
@Component({
selector: 'pc-action',
templateUrl: './action.component.html',
styleUrls: ['./action.component.scss']
})
export class ActionComponent {
selectedIndex: number;
operationArr: OperationType[] = [
{
title: $localize`Pre-request Script`,
type: 'pre'
},
{
title: $localize`After-response Script`,
type: 'after'
}
];
type: string = 'pre';
typeChange(type) {
if (type === this.type) return;
this.type = type;
}
}

View File

@ -1,13 +1,11 @@
import { Component, Input, OnChanges, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { EoNgFeedbackMessageService } from 'eo-ng-feedback';
import { ApiMockService } from 'pc/browser/src/app/pages/workspace/project/api/http/mock/api-mock.service';
import { ApiMockEditComponent } from 'pc/browser/src/app/pages/workspace/project/api/http/mock/edit/api-mock-edit.component';
import { ModalService } from 'pc/browser/src/app/services/modal.service';
import { Mock, MockCreateWay } from 'pc/browser/src/app/services/storage/db/models';
import { ApiData } from 'pc/browser/src/app/services/storage/db/models/apiData';
import { eoDeepCopy, copy } from 'pc/browser/src/app/shared/utils/index.utils';
import { ApiData } from '../../../../../services/storage/db/models/apiData';
import { ApiMockEntity } from '../../../../../services/storage/index.model';
@Component({
selector: 'eo-api-mock-table',
template: ` <eo-ng-table-pro [columns]="mockListColumns" [nzData]="mockList"></eo-ng-table-pro>
@ -36,19 +34,13 @@ export class ApiMockTableComponent implements OnInit, OnChanges {
mockListColumns = [];
mockPrefix: string;
mockList: ApiMockEntity[] = [];
mockList: Array<{ url: string } & Mock> = [];
constructor(private message: EoNgFeedbackMessageService, private modal: ModalService, private apiMock: ApiMockService) {}
ngOnInit() {
this.initTable();
}
async handleDeleteMockItem(item, index) {
await this.apiMock.deleteMock(item.id);
this.mockList.splice(index, 1)[0];
this.mockList = [...this.mockList];
this.message.success($localize`Delete Succeeded`);
}
private initTable() {
this.mockListColumns = [
@ -58,45 +50,19 @@ export class ApiMockTableComponent implements OnInit, OnChanges {
key: 'createWay',
width: 150,
enums: [
{ title: $localize`System creation`, value: 'system' },
{ title: $localize`Manual creation`, value: 'custom' }
{ title: $localize`System creation`, value: MockCreateWay.System },
{ title: $localize`Manual creation`, value: MockCreateWay.Custom }
]
},
{ title: 'URL', slot: this.urlCell },
{
type: 'btnList',
btns: [
{
title: $localize`:@@MockPreview:Preview`,
icon: 'preview-open',
click: item => {
const modal = this.modal.create({
nzTitle: $localize`Preview Mock`,
nzWidth: '70%',
nzContent: ApiMockEditComponent,
nzComponentParams: {
model: item.data,
isEdit: false
}
});
}
},
{
action: 'edit',
showFn: item => item.data.createWay !== 'system',
click: (item, index) => {
const modal = this.modal.create({
nzTitle: $localize`Edit Mock`,
nzWidth: '70%',
nzContent: ApiMockEditComponent,
nzComponentParams: {
model: eoDeepCopy(item.data)
},
nzOnOk: async () => {
await this.addOrEditModal(modal.componentInstance.model, index);
modal.destroy();
}
});
this.apiMock.toEdit(item.data);
}
},
{
@ -104,7 +70,7 @@ export class ApiMockTableComponent implements OnInit, OnChanges {
showFn: item => item.data.createWay !== 'system',
confirm: true,
confirmFn: (item, index) => {
this.handleDeleteMockItem(item.data, index);
this.apiMock.toDelete(item.data.id);
}
}
]
@ -119,7 +85,9 @@ export class ApiMockTableComponent implements OnInit, OnChanges {
item.response = this.apiMock.getMockResponseByAPI(this.apiData);
}
});
console.log(this.apiData);
this.mockPrefix = this.apiMock.getMockPrefix(this.apiData);
console.log(this.mockPrefix);
this.setMocksUrl();
}
}

View File

@ -1,5 +1,16 @@
<div class="flex w-full eo-api-script">
<div class="w-[300px] overflow-auto border-0 border-r-[1px] border-solid border-[#f0f0f0]">
<div class="flex-1 overflow-hidden">
<eo-monaco-editor
[(code)]="code"
[config]="{ language: 'javascript' }"
[autoHeight]="true"
[eventList]="['format', 'copy', 'search', 'replace']"
(codeChange)="handleChange($event)"
[completions]="completions"
>
</eo-monaco-editor>
</div>
<div class="w-[230px] overflow-auto border-0 border-l-[1px] border-solid border-[#f0f0f0]">
<div class="flex justify-between p-3">
<div i18n>Snippets</div>
<div>
@ -48,15 +59,4 @@
</ng-template>
</ng-template>
</div>
<div class="flex-1 overflow-hidden">
<eo-monaco-editor
[(code)]="code"
[config]="{ language: 'javascript' }"
[autoHeight]="true"
[eventList]="['format', 'copy', 'search', 'replace']"
(codeChange)="handleChange($event)"
[completions]="completions"
>
</eo-monaco-editor>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More