refactor: file prefix replace utils & add unit test (#1676)
* refactor: file prefix replace utils * chore: add unit tests for core module
This commit is contained in:
parent
fc3a5c9e01
commit
99d083d84a
1
Makefile
1
Makefile
@ -39,6 +39,7 @@ lint: check-file-counts
|
||||
# Testing
|
||||
test: lint
|
||||
yarn build:test
|
||||
yarn test:unit
|
||||
yarn test
|
||||
|
||||
# Builds and publishes the app
|
||||
|
||||
3
core/.gitignore
vendored
3
core/.gitignore
vendored
@ -6,7 +6,4 @@ coverage
|
||||
.vscode
|
||||
.idea
|
||||
dist
|
||||
compiled
|
||||
.awcache
|
||||
.rpt2_cache
|
||||
docs
|
||||
|
||||
7
core/jest.config.js
Normal file
7
core/jest.config.js
Normal file
@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
moduleNameMapper: {
|
||||
'@/(.*)': '<rootDir>/src/$1',
|
||||
},
|
||||
}
|
||||
@ -12,7 +12,8 @@
|
||||
"module": "dist/core.es5.js",
|
||||
"typings": "dist/types/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
"dist",
|
||||
"types"
|
||||
],
|
||||
"author": "Jan <service@jan.ai>",
|
||||
"exports": {
|
||||
@ -38,18 +39,23 @@
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "tslint --project tsconfig.json -t codeFrame 'src/**/*.ts' 'test/**/*.ts'",
|
||||
"test": "jest",
|
||||
"prebuild": "rimraf dist",
|
||||
"build": "tsc --module commonjs && rollup -c rollup.config.ts",
|
||||
"start": "rollup -c rollup.config.ts -w"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^25.4.0",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/node": "^12.0.2",
|
||||
"eslint-plugin-jest": "^23.8.2",
|
||||
"rollup": "^2.38.5",
|
||||
"rollup-plugin-commonjs": "^9.1.8",
|
||||
"rollup-plugin-json": "^3.1.0",
|
||||
"rollup-plugin-node-resolve": "^5.2.0",
|
||||
"rollup-plugin-sourcemaps": "^0.6.3",
|
||||
"rollup-plugin-typescript2": "^0.36.0",
|
||||
"ts-jest": "^26.1.1",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.2.2"
|
||||
}
|
||||
|
||||
@ -1,58 +1,58 @@
|
||||
import { DownloadRoute } from '../../../api'
|
||||
import { join } from 'path'
|
||||
import { userSpacePath } from '../../extension/manager'
|
||||
import { DownloadManager } from '../../download'
|
||||
import { HttpServer } from '../HttpServer'
|
||||
import { createWriteStream } from 'fs'
|
||||
import { DownloadRoute } from "../../../api";
|
||||
import { join } from "path";
|
||||
import { userSpacePath } from "../../extension/manager";
|
||||
import { DownloadManager } from "../../download";
|
||||
import { HttpServer } from "../HttpServer";
|
||||
import { createWriteStream } from "fs";
|
||||
import { normalizeFilePath } from "../../path";
|
||||
|
||||
export const downloadRouter = async (app: HttpServer) => {
|
||||
app.post(`/${DownloadRoute.downloadFile}`, async (req, res) => {
|
||||
const strictSSL = !(req.query.ignoreSSL === 'true');
|
||||
const proxy = req.query.proxy?.startsWith('http') ? req.query.proxy : undefined;
|
||||
const body = JSON.parse(req.body as any)
|
||||
const strictSSL = !(req.query.ignoreSSL === "true");
|
||||
const proxy = req.query.proxy?.startsWith("http") ? req.query.proxy : undefined;
|
||||
const body = JSON.parse(req.body as any);
|
||||
const normalizedArgs = body.map((arg: any) => {
|
||||
if (typeof arg === 'string' && arg.includes('file:/')) {
|
||||
return join(userSpacePath, arg.replace('file:/', ''))
|
||||
if (typeof arg === "string") {
|
||||
return join(userSpacePath, normalizeFilePath(arg));
|
||||
}
|
||||
return arg
|
||||
})
|
||||
return arg;
|
||||
});
|
||||
|
||||
const localPath = normalizedArgs[1]
|
||||
const fileName = localPath.split('/').pop() ?? ''
|
||||
const localPath = normalizedArgs[1];
|
||||
const fileName = localPath.split("/").pop() ?? "";
|
||||
|
||||
const request = require('request')
|
||||
const progress = require('request-progress')
|
||||
|
||||
const rq = request({ url: normalizedArgs[0], strictSSL, proxy })
|
||||
const request = require("request");
|
||||
const progress = require("request-progress");
|
||||
|
||||
const rq = request({ url: normalizedArgs[0], strictSSL, proxy });
|
||||
progress(rq, {})
|
||||
.on('progress', function (state: any) {
|
||||
console.log('download onProgress', state)
|
||||
.on("progress", function (state: any) {
|
||||
console.log("download onProgress", state);
|
||||
})
|
||||
.on('error', function (err: Error) {
|
||||
console.log('download onError', err)
|
||||
.on("error", function (err: Error) {
|
||||
console.log("download onError", err);
|
||||
})
|
||||
.on('end', function () {
|
||||
console.log('download onEnd')
|
||||
.on("end", function () {
|
||||
console.log("download onEnd");
|
||||
})
|
||||
.pipe(createWriteStream(normalizedArgs[1]))
|
||||
.pipe(createWriteStream(normalizedArgs[1]));
|
||||
|
||||
DownloadManager.instance.setRequest(fileName, rq)
|
||||
})
|
||||
DownloadManager.instance.setRequest(fileName, rq);
|
||||
});
|
||||
|
||||
app.post(`/${DownloadRoute.abortDownload}`, async (req, res) => {
|
||||
const body = JSON.parse(req.body as any)
|
||||
const body = JSON.parse(req.body as any);
|
||||
const normalizedArgs = body.map((arg: any) => {
|
||||
if (typeof arg === 'string' && arg.includes('file:/')) {
|
||||
return join(userSpacePath, arg.replace('file:/', ''))
|
||||
if (typeof arg === "string") {
|
||||
return join(userSpacePath, normalizeFilePath(arg));
|
||||
}
|
||||
return arg
|
||||
})
|
||||
return arg;
|
||||
});
|
||||
|
||||
const localPath = normalizedArgs[0]
|
||||
const fileName = localPath.split('/').pop() ?? ''
|
||||
console.debug('fileName', fileName)
|
||||
const rq = DownloadManager.instance.networkRequests[fileName]
|
||||
DownloadManager.instance.networkRequests[fileName] = undefined
|
||||
rq?.abort()
|
||||
})
|
||||
}
|
||||
const localPath = normalizedArgs[0];
|
||||
const fileName = localPath.split("/").pop() ?? "";
|
||||
const rq = DownloadManager.instance.networkRequests[fileName];
|
||||
DownloadManager.instance.networkRequests[fileName] = undefined;
|
||||
rq?.abort();
|
||||
});
|
||||
};
|
||||
|
||||
@ -6,3 +6,4 @@ export * from './download'
|
||||
export * from './module'
|
||||
export * from './api'
|
||||
export * from './log'
|
||||
export * from './path'
|
||||
|
||||
9
core/src/node/path.ts
Normal file
9
core/src/node/path.ts
Normal file
@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Normalize file path
|
||||
* Remove all file protocol prefix
|
||||
* @param path
|
||||
* @returns
|
||||
*/
|
||||
export function normalizeFilePath(path: string): string {
|
||||
return path.replace(/^(file:[\\/]+)([^:\s]+)$/, "$2");
|
||||
}
|
||||
12
core/tests/node/path.test.ts
Normal file
12
core/tests/node/path.test.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { normalizeFilePath } from "../../src/node/path";
|
||||
|
||||
describe("Test file normalize", () => {
|
||||
test("returns no file protocol prefix on Unix", async () => {
|
||||
expect(normalizeFilePath("file://test.txt")).toBe("test.txt");
|
||||
expect(normalizeFilePath("file:/test.txt")).toBe("test.txt");
|
||||
});
|
||||
test("returns no file protocol prefix on Windows", async () => {
|
||||
expect(normalizeFilePath("file:\\\\test.txt")).toBe("test.txt");
|
||||
expect(normalizeFilePath("file:\\test.txt")).toBe("test.txt");
|
||||
});
|
||||
});
|
||||
@ -13,7 +13,7 @@
|
||||
"declarationDir": "dist/types",
|
||||
"outDir": "dist/lib",
|
||||
"importHelpers": true,
|
||||
"typeRoots": ["node_modules/@types"]
|
||||
"types": ["@types/jest"]
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ import request from 'request'
|
||||
import { createWriteStream, renameSync } from 'fs'
|
||||
import { DownloadEvent, DownloadRoute } from '@janhq/core'
|
||||
const progress = require('request-progress')
|
||||
import { DownloadManager } from '@janhq/core/node'
|
||||
import { DownloadManager, normalizeFilePath } from '@janhq/core/node'
|
||||
|
||||
export function handleDownloaderIPCs() {
|
||||
/**
|
||||
@ -54,66 +54,68 @@ export function handleDownloaderIPCs() {
|
||||
* @param url - The URL to download the file from.
|
||||
* @param fileName - The name to give the downloaded file.
|
||||
*/
|
||||
ipcMain.handle(DownloadRoute.downloadFile, async (_event, url, fileName, network) => {
|
||||
const strictSSL = !network?.ignoreSSL;
|
||||
const proxy = network?.proxy?.startsWith('http') ? network.proxy : undefined;
|
||||
const userDataPath = join(app.getPath('home'), 'jan')
|
||||
if (
|
||||
typeof fileName === 'string' &&
|
||||
(fileName.includes('file:/') || fileName.includes('file:\\'))
|
||||
) {
|
||||
fileName = fileName.replace('file:/', '').replace('file:\\', '')
|
||||
}
|
||||
const destination = resolve(userDataPath, fileName)
|
||||
const rq = request({ url, strictSSL, proxy })
|
||||
ipcMain.handle(
|
||||
DownloadRoute.downloadFile,
|
||||
async (_event, url, fileName, network) => {
|
||||
const strictSSL = !network?.ignoreSSL
|
||||
const proxy = network?.proxy?.startsWith('http')
|
||||
? network.proxy
|
||||
: undefined
|
||||
const userDataPath = join(app.getPath('home'), 'jan')
|
||||
if (typeof fileName === 'string') {
|
||||
fileName = normalizeFilePath(fileName)
|
||||
}
|
||||
const destination = resolve(userDataPath, fileName)
|
||||
const rq = request({ url, strictSSL, proxy })
|
||||
|
||||
// Put request to download manager instance
|
||||
DownloadManager.instance.setRequest(fileName, rq)
|
||||
// Put request to download manager instance
|
||||
DownloadManager.instance.setRequest(fileName, rq)
|
||||
|
||||
// Downloading file to a temp file first
|
||||
const downloadingTempFile = `${destination}.download`
|
||||
|
||||
progress(rq, {})
|
||||
.on('progress', function (state: any) {
|
||||
WindowManager?.instance.currentWindow?.webContents.send(
|
||||
DownloadEvent.onFileDownloadUpdate,
|
||||
{
|
||||
...state,
|
||||
fileName,
|
||||
}
|
||||
)
|
||||
})
|
||||
.on('error', function (err: Error) {
|
||||
WindowManager?.instance.currentWindow?.webContents.send(
|
||||
DownloadEvent.onFileDownloadError,
|
||||
{
|
||||
fileName,
|
||||
err,
|
||||
}
|
||||
)
|
||||
})
|
||||
.on('end', function () {
|
||||
if (DownloadManager.instance.networkRequests[fileName]) {
|
||||
// Finished downloading, rename temp file to actual file
|
||||
renameSync(downloadingTempFile, destination)
|
||||
// Downloading file to a temp file first
|
||||
const downloadingTempFile = `${destination}.download`
|
||||
|
||||
progress(rq, {})
|
||||
.on('progress', function (state: any) {
|
||||
WindowManager?.instance.currentWindow?.webContents.send(
|
||||
DownloadEvent.onFileDownloadSuccess,
|
||||
DownloadEvent.onFileDownloadUpdate,
|
||||
{
|
||||
...state,
|
||||
fileName,
|
||||
}
|
||||
)
|
||||
DownloadManager.instance.setRequest(fileName, undefined)
|
||||
} else {
|
||||
})
|
||||
.on('error', function (err: Error) {
|
||||
WindowManager?.instance.currentWindow?.webContents.send(
|
||||
DownloadEvent.onFileDownloadError,
|
||||
{
|
||||
fileName,
|
||||
err: { message: 'aborted' },
|
||||
err,
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
.pipe(createWriteStream(downloadingTempFile))
|
||||
})
|
||||
})
|
||||
.on('end', function () {
|
||||
if (DownloadManager.instance.networkRequests[fileName]) {
|
||||
// Finished downloading, rename temp file to actual file
|
||||
renameSync(downloadingTempFile, destination)
|
||||
|
||||
WindowManager?.instance.currentWindow?.webContents.send(
|
||||
DownloadEvent.onFileDownloadSuccess,
|
||||
{
|
||||
fileName,
|
||||
}
|
||||
)
|
||||
DownloadManager.instance.setRequest(fileName, undefined)
|
||||
} else {
|
||||
WindowManager?.instance.currentWindow?.webContents.send(
|
||||
DownloadEvent.onFileDownloadError,
|
||||
{
|
||||
fileName,
|
||||
err: { message: 'aborted' },
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
.pipe(createWriteStream(downloadingTempFile))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import { userSpacePath, getResourcePath } from './../utils/path'
|
||||
import fs from 'fs'
|
||||
import { join } from 'path'
|
||||
import { FileStat } from '@janhq/core'
|
||||
import { normalizeFilePath } from '@janhq/core/node'
|
||||
|
||||
/**
|
||||
* Handles file system extensions operations.
|
||||
@ -42,11 +43,7 @@ export function handleFileMangerIPCs() {
|
||||
ipcMain.handle(
|
||||
FileManagerRoute.fileStat,
|
||||
async (_event, path: string): Promise<FileStat | undefined> => {
|
||||
const normalizedPath = path
|
||||
.replace(`file://`, '')
|
||||
.replace(`file:/`, '')
|
||||
.replace(`file:\\\\`, '')
|
||||
.replace(`file:\\`, '')
|
||||
const normalizedPath = normalizeFilePath(path)
|
||||
|
||||
const fullPath = join(userSpacePath, normalizedPath)
|
||||
const isExist = fs.existsSync(fullPath)
|
||||
|
||||
@ -3,6 +3,7 @@ import { ipcMain } from 'electron'
|
||||
import { FileSystemRoute } from '@janhq/core'
|
||||
import { userSpacePath } from '../utils/path'
|
||||
import { join } from 'path'
|
||||
import { normalizeFilePath } from '@janhq/core/node'
|
||||
/**
|
||||
* Handles file system operations.
|
||||
*/
|
||||
@ -15,14 +16,7 @@ export function handleFsIPCs() {
|
||||
...args.map((arg) =>
|
||||
typeof arg === 'string' &&
|
||||
(arg.includes(`file:/`) || arg.includes(`file:\\`))
|
||||
? join(
|
||||
userSpacePath,
|
||||
arg
|
||||
.replace(`file://`, '')
|
||||
.replace(`file:/`, '')
|
||||
.replace(`file:\\\\`, '')
|
||||
.replace(`file:\\`, '')
|
||||
)
|
||||
? join(userSpacePath, normalizeFilePath(arg))
|
||||
: arg
|
||||
)
|
||||
)
|
||||
|
||||
@ -11,19 +11,15 @@
|
||||
],
|
||||
"nohoist": [
|
||||
"uikit",
|
||||
"uikit/*",
|
||||
"core",
|
||||
"core/*",
|
||||
"electron",
|
||||
"electron/**",
|
||||
"web",
|
||||
"web/**",
|
||||
"server",
|
||||
"server/**"
|
||||
"server"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "yarn workspace jan lint && yarn workspace jan-web lint",
|
||||
"test:unit": "yarn workspace @janhq/core test",
|
||||
"test": "yarn workspace jan test:e2e",
|
||||
"copy:assets": "cpx \"models/**\" \"electron/models/\" && cpx \"docs/openapi/**\" \"electron/docs/openapi\"",
|
||||
"dev:electron": "yarn copy:assets && yarn workspace jan dev",
|
||||
|
||||
@ -29,7 +29,8 @@ export default function FeatureToggleWrapper({
|
||||
const EXPERIMENTAL_FEATURE = 'experimentalFeature'
|
||||
const IGNORE_SSL = 'ignoreSSLFeature'
|
||||
const HTTPS_PROXY_FEATURE = 'httpsProxyFeature'
|
||||
const [experimentalFeature, directSetExperimentalFeature] = useState<boolean>(false)
|
||||
const [experimentalFeature, directSetExperimentalFeature] =
|
||||
useState<boolean>(false)
|
||||
const [ignoreSSL, directSetIgnoreSSL] = useState<boolean>(false)
|
||||
const [proxy, directSetProxy] = useState<string>('')
|
||||
|
||||
@ -37,12 +38,8 @@ export default function FeatureToggleWrapper({
|
||||
directSetExperimentalFeature(
|
||||
localStorage.getItem(EXPERIMENTAL_FEATURE) === 'true'
|
||||
)
|
||||
directSetIgnoreSSL(
|
||||
localStorage.getItem(IGNORE_SSL) === 'true'
|
||||
)
|
||||
directSetProxy(
|
||||
localStorage.getItem(HTTPS_PROXY_FEATURE) ?? ""
|
||||
)
|
||||
directSetIgnoreSSL(localStorage.getItem(IGNORE_SSL) === 'true')
|
||||
directSetProxy(localStorage.getItem(HTTPS_PROXY_FEATURE) ?? '')
|
||||
}, [])
|
||||
|
||||
const setExperimentalFeature = (on: boolean) => {
|
||||
|
||||
@ -104,8 +104,8 @@ export const useCreateNewThread = () => {
|
||||
}
|
||||
|
||||
extensionManager
|
||||
.get<ConversationalExtension>(ExtensionType.Conversational)
|
||||
?.saveThread(thread)
|
||||
.get<ConversationalExtension>(ExtensionType.Conversational)
|
||||
?.saveThread(thread)
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { useContext } from 'react'
|
||||
|
||||
import {
|
||||
Model,
|
||||
ExtensionType,
|
||||
@ -8,14 +10,14 @@ import {
|
||||
|
||||
import { useSetAtom } from 'jotai'
|
||||
|
||||
import { FeatureToggleContext } from '@/context/FeatureToggle'
|
||||
|
||||
import { modelBinFileName } from '@/utils/model'
|
||||
|
||||
import { useDownloadState } from './useDownloadState'
|
||||
|
||||
import { extensionManager } from '@/extension/ExtensionManager'
|
||||
import { addNewDownloadingModelAtom } from '@/helpers/atoms/Model.atom'
|
||||
import { useContext } from 'react'
|
||||
import { FeatureToggleContext } from '@/context/FeatureToggle'
|
||||
|
||||
export default function useDownloadModel() {
|
||||
const { ignoreSSL, proxy } = useContext(FeatureToggleContext)
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --write \"**/*.{js,jsx,ts,tsx}\"",
|
||||
"compile": "tsc --noEmit -p . --pretty"
|
||||
},
|
||||
|
||||
@ -1,19 +1,16 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
|
||||
import { useContext, useEffect, useState, useCallback, ChangeEvent } from 'react'
|
||||
import {
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
useCallback,
|
||||
ChangeEvent,
|
||||
} from 'react'
|
||||
|
||||
import { fs } from '@janhq/core'
|
||||
import {
|
||||
Switch,
|
||||
Button,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalTitle,
|
||||
ModalTrigger,
|
||||
} from '@janhq/uikit'
|
||||
import { Switch, Button, Input } from '@janhq/uikit'
|
||||
|
||||
import ShortcutModal from '@/containers/ShortcutModal'
|
||||
|
||||
@ -24,22 +21,30 @@ import { FeatureToggleContext } from '@/context/FeatureToggle'
|
||||
import { useSettings } from '@/hooks/useSettings'
|
||||
|
||||
const Advanced = () => {
|
||||
const { experimentalFeature, setExperimentalFeature, ignoreSSL, setIgnoreSSL, proxy, setProxy } =
|
||||
useContext(FeatureToggleContext)
|
||||
const {
|
||||
experimentalFeature,
|
||||
setExperimentalFeature,
|
||||
ignoreSSL,
|
||||
setIgnoreSSL,
|
||||
proxy,
|
||||
setProxy,
|
||||
} = useContext(FeatureToggleContext)
|
||||
const [partialProxy, setPartialProxy] = useState<string>(proxy)
|
||||
const [gpuEnabled, setGpuEnabled] = useState<boolean>(false)
|
||||
const { readSettings, saveSettings, validateSettings, setShowNotification } =
|
||||
useSettings()
|
||||
const onProxyChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.target.value || ''
|
||||
setPartialProxy(value)
|
||||
if (value.trim().startsWith('http')) {
|
||||
setProxy(value.trim())
|
||||
}
|
||||
else {
|
||||
setProxy('')
|
||||
}
|
||||
}, [setPartialProxy, setProxy])
|
||||
const onProxyChange = useCallback(
|
||||
(event: ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.target.value || ''
|
||||
setPartialProxy(value)
|
||||
if (value.trim().startsWith('http')) {
|
||||
setProxy(value.trim())
|
||||
} else {
|
||||
setProxy('')
|
||||
}
|
||||
},
|
||||
[setPartialProxy, setProxy]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
readSettings().then((settings) => {
|
||||
@ -115,15 +120,14 @@ const Advanced = () => {
|
||||
<div className="flex w-full items-start justify-between border-b border-border py-4 first:pt-0 last:border-none">
|
||||
<div className="w-4/5 flex-shrink-0 space-y-1.5">
|
||||
<div className="flex gap-x-2">
|
||||
<h6 className="text-sm font-semibold capitalize">
|
||||
HTTPS Proxy
|
||||
</h6>
|
||||
<h6 className="text-sm font-semibold capitalize">HTTPS Proxy</h6>
|
||||
</div>
|
||||
<p className="whitespace-pre-wrap leading-relaxed">
|
||||
Specify the HTTPS proxy or leave blank (proxy auto-configuration and SOCKS not supported).
|
||||
Specify the HTTPS proxy or leave blank (proxy auto-configuration and
|
||||
SOCKS not supported).
|
||||
</p>
|
||||
<Input
|
||||
placeholder={"http://<user>:<password>@<domain or IP>:<port>"}
|
||||
placeholder={'http://<user>:<password>@<domain or IP>:<port>'}
|
||||
value={partialProxy}
|
||||
onChange={onProxyChange}
|
||||
/>
|
||||
@ -138,7 +142,8 @@ const Advanced = () => {
|
||||
</h6>
|
||||
</div>
|
||||
<p className="whitespace-pre-wrap leading-relaxed">
|
||||
Allow self-signed or unverified certificates - may be required for certain proxies.
|
||||
Allow self-signed or unverified certificates - may be required for
|
||||
certain proxies.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
|
||||
@ -1,101 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
type Props = {
|
||||
extensionName: string
|
||||
preferenceValues: any
|
||||
preferenceItems: any
|
||||
}
|
||||
|
||||
import { useForm } from 'react-hook-form'
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
Input,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Button,
|
||||
} from '@janhq/uikit'
|
||||
|
||||
import * as z from 'zod'
|
||||
|
||||
import { toaster } from '@/containers/Toast'
|
||||
|
||||
import { formatExtensionsName } from '@/utils/converter'
|
||||
|
||||
const PreferenceExtensions = (props: Props) => {
|
||||
const { extensionName, preferenceValues, preferenceItems } = props
|
||||
|
||||
const FormSchema = z.record(
|
||||
z
|
||||
.string({ required_error: 'Field is Required' })
|
||||
.min(1, { message: 'Field is Required' })
|
||||
)
|
||||
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: preferenceValues.reduce(
|
||||
(obj: any, item: { key: any; value: any }) =>
|
||||
Object.assign(obj, { [item.key]: item.value }),
|
||||
{}
|
||||
),
|
||||
})
|
||||
|
||||
const onSubmit = async (values: z.infer<typeof FormSchema>) => {
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
// await preferences.set(extensionName, key, value)
|
||||
// await execute(ExtensionService.OnPreferencesUpdate, {})
|
||||
}
|
||||
toaster({
|
||||
title: formatExtensionsName(extensionName),
|
||||
description: 'Successfully updated preferences',
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full lg:mt-10 lg:w-1/2">
|
||||
<h6 className="mb-6 text-lg font-semibold capitalize">
|
||||
{formatExtensionsName(extensionName)}
|
||||
</h6>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
{preferenceItems
|
||||
.filter((x: any) => x.extensionName === extensionName)
|
||||
?.map((e: any) => (
|
||||
<FormField
|
||||
key={e.preferenceKey}
|
||||
control={form.control}
|
||||
name={e.preferenceKey}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{e.preferenceName}</FormLabel>
|
||||
<FormDescription className="mb-2">
|
||||
{e.preferenceDescription}
|
||||
</FormDescription>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={`Enter your ${e.preferenceName}`}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
<div className="pt-4">
|
||||
<Button type="submit" block>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PreferenceExtensions
|
||||
@ -1,38 +1,17 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
|
||||
import React, { useState, useEffect, useRef, useContext } from 'react'
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
|
||||
import { Button } from '@janhq/uikit'
|
||||
|
||||
import { FeatureToggleContext } from '@/context/FeatureToggle'
|
||||
|
||||
import { formatExtensionsName } from '@/utils/converter'
|
||||
|
||||
import { extensionManager } from '@/extension'
|
||||
import Extension from '@/extension/Extension'
|
||||
|
||||
const ExtensionCatalog = () => {
|
||||
const [activeExtensions, setActiveExtensions] = useState<any[]>([])
|
||||
const [extensionCatalog, setExtensionCatalog] = useState<any[]>([])
|
||||
const [activeExtensions, setActiveExtensions] = useState<Extension[]>([])
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null)
|
||||
const { experimentalFeature } = useContext(FeatureToggleContext)
|
||||
/**
|
||||
* Loads the extension catalog module from a CDN and sets it as the extension catalog state.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!window.electronAPI) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get extension manifest
|
||||
import(/* webpackIgnore: true */ PLUGIN_CATALOG + `?t=${Date.now()}`).then(
|
||||
(data) => {
|
||||
if (Array.isArray(data.default) && experimentalFeature)
|
||||
setExtensionCatalog(data.default)
|
||||
}
|
||||
)
|
||||
}, [experimentalFeature])
|
||||
|
||||
/**
|
||||
* Fetches the active extensions and their preferences from the `extensions` and `preferences` modules.
|
||||
* If the `experimentComponent` extension point is available, it executes the extension point and
|
||||
@ -90,57 +69,28 @@ const ExtensionCatalog = () => {
|
||||
|
||||
return (
|
||||
<div className="block w-full">
|
||||
{extensionCatalog
|
||||
.concat(
|
||||
activeExtensions.filter(
|
||||
(e) => !(extensionCatalog ?? []).some((p) => p.name === e.name)
|
||||
) ?? []
|
||||
)
|
||||
.map((item, i) => {
|
||||
const isActiveExtension = activeExtensions.some(
|
||||
(x) => x.name === item.name
|
||||
)
|
||||
const installedExtension = activeExtensions.filter(
|
||||
(p) => p.name === item.name
|
||||
)[0]
|
||||
const updateVersionExtensions = Number(
|
||||
installedExtension?.version.replaceAll('.', '')
|
||||
)
|
||||
|
||||
const hasUpdateVersionExtensions =
|
||||
item.version.replaceAll('.', '') > updateVersionExtensions
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="flex w-full items-start justify-between border-b border-border py-4 first:pt-0 last:border-none"
|
||||
>
|
||||
<div className="w-4/5 flex-shrink-0 space-y-1.5">
|
||||
<div className="flex gap-x-2">
|
||||
<h6 className="text-sm font-semibold capitalize">
|
||||
{formatExtensionsName(item.name)}
|
||||
</h6>
|
||||
<p className="whitespace-pre-wrap font-semibold leading-relaxed ">
|
||||
v{item.version}
|
||||
</p>
|
||||
</div>
|
||||
<p className="whitespace-pre-wrap leading-relaxed ">
|
||||
{item.description}
|
||||
{activeExtensions.map((item, i) => {
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="flex w-full items-start justify-between border-b border-border py-4 first:pt-0 last:border-none"
|
||||
>
|
||||
<div className="w-4/5 flex-shrink-0 space-y-1.5">
|
||||
<div className="flex gap-x-2">
|
||||
<h6 className="text-sm font-semibold capitalize">
|
||||
{formatExtensionsName(item.name ?? item.description ?? '')}
|
||||
</h6>
|
||||
<p className="whitespace-pre-wrap font-semibold leading-relaxed ">
|
||||
v{item.version}
|
||||
</p>
|
||||
{isActiveExtension && (
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="whitespace-pre-wrap leading-relaxed ">
|
||||
Installed{' '}
|
||||
{hasUpdateVersionExtensions
|
||||
? `v${installedExtension.version}`
|
||||
: 'the latest version'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="whitespace-pre-wrap leading-relaxed ">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{/* Manual Installation */}
|
||||
<div className="flex w-full items-start justify-between border-b border-border py-4 first:pt-0 last:border-none">
|
||||
<div className="w-4/5 flex-shrink-0 space-y-1.5">
|
||||
@ -9,8 +9,7 @@ import { twMerge } from 'tailwind-merge'
|
||||
|
||||
import Advanced from '@/screens/Settings/Advanced'
|
||||
import AppearanceOptions from '@/screens/Settings/Appearance'
|
||||
import ExtensionCatalog from '@/screens/Settings/CoreExtensions/ExtensionsCatalog'
|
||||
import PreferenceExtensions from '@/screens/Settings/CoreExtensions/PreferenceExtensions'
|
||||
import ExtensionCatalog from '@/screens/Settings/CoreExtensions'
|
||||
|
||||
import Models from '@/screens/Settings/Models'
|
||||
|
||||
@ -19,8 +18,6 @@ import { formatExtensionsName } from '@/utils/converter'
|
||||
const SettingsScreen = () => {
|
||||
const [activeStaticMenu, setActiveStaticMenu] = useState('My Models')
|
||||
const [menus, setMenus] = useState<any[]>([])
|
||||
const [preferenceItems, setPreferenceItems] = useState<any[]>([])
|
||||
const [preferenceValues, setPreferenceValues] = useState<any[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
const menu = ['My Models', 'My Settings', 'Advanced Settings']
|
||||
@ -31,12 +28,6 @@ const SettingsScreen = () => {
|
||||
setMenus(menu)
|
||||
}, [])
|
||||
|
||||
const preferenceExtensions = preferenceItems
|
||||
.map((x) => x.extensionnName)
|
||||
.filter((x, i) => {
|
||||
// return prefere/nceItems.map((x) => x.extensionName).indexOf(x) === i
|
||||
})
|
||||
|
||||
const [activePreferenceExtension, setActivePreferenceExtension] = useState('')
|
||||
|
||||
const handleShowOptions = (menu: string) => {
|
||||
@ -52,15 +43,6 @@ const SettingsScreen = () => {
|
||||
|
||||
case 'My Models':
|
||||
return <Models />
|
||||
|
||||
default:
|
||||
return (
|
||||
<PreferenceExtensions
|
||||
extensionName={menu}
|
||||
preferenceItems={preferenceItems}
|
||||
preferenceValues={preferenceValues}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -97,45 +79,6 @@ const SettingsScreen = () => {
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex-shrink-0">
|
||||
{preferenceExtensions.length > 0 && (
|
||||
<label className="font-bold uppercase text-muted-foreground">
|
||||
Core Extensions
|
||||
</label>
|
||||
)}
|
||||
<div className="mt-2 font-medium">
|
||||
{preferenceExtensions.map((menu, i) => {
|
||||
const isActive = activePreferenceExtension === menu
|
||||
return (
|
||||
<div key={i} className="relative my-0.5 block py-1.5">
|
||||
<div
|
||||
onClick={() => {
|
||||
setActivePreferenceExtension(menu)
|
||||
setActiveStaticMenu('')
|
||||
}}
|
||||
className="block w-full cursor-pointer"
|
||||
>
|
||||
<span
|
||||
className={twMerge(
|
||||
'capitalize',
|
||||
isActive && 'relative z-10'
|
||||
)}
|
||||
>
|
||||
{formatExtensionsName(String(menu))}
|
||||
</span>
|
||||
</div>
|
||||
{isActive ? (
|
||||
<m.div
|
||||
className="absolute inset-0 -left-3 h-full w-[calc(100%+24px)] rounded-md bg-primary/50"
|
||||
layoutId="active-static-menu"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user