feat: 去掉antd

This commit is contained in:
xion 2025-03-20 21:47:50 +08:00
parent c206add7eb
commit cfd263a1e7
36 changed files with 1369 additions and 769 deletions

View File

@ -20,7 +20,7 @@
"@kevisual/center-components": "workspace:*",
"@kevisual/codemirror": "workspace:*",
"@kevisual/container": "1.0.0",
"@kevisual/query": "^0.0.8",
"@kevisual/query": "^0.0.9",
"@kevisual/resources": "workspace:*",
"@kevisual/system-ui": "^0.0.3",
"@kevisual/ui": "^0.0.2",
@ -41,14 +41,14 @@
"immer": "^10.1.1",
"lodash-es": "^4.17.21",
"marked": "^15.0.7",
"nanoid": "^5.1.4",
"nanoid": "^5.1.5",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-hook-form": "^7.54.2",
"react-i18next": "^15.4.1",
"react-resizable-panels": "^2.1.7",
"react-router": "^7.3.0",
"react-router-dom": "^7.3.0",
"react-router": "^7.4.0",
"react-router-dom": "^7.4.0",
"react-toastify": "^11.0.5",
"vite-plugin-tsconfig-paths": "^1.4.1",
"zustand": "^5.0.3"
@ -61,7 +61,7 @@
"@types/lodash-es": "^4.17.12",
"@types/node": "^22.13.10",
"@types/path-browserify": "^1.0.3",
"@types/react": "^19.0.11",
"@types/react": "^19.0.12",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-basic-ssl": "^2.0.0",
"@vitejs/plugin-react": "^4.3.4",
@ -71,7 +71,7 @@
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"lucide-react": "^0.482.0",
"lucide-react": "^0.483.0",
"path-browserify": "^1.0.1",
"postcss-import": "^16.1.0",
"pretty-bytes": "^6.1.1",
@ -80,7 +80,7 @@
"tailwindcss": "^4.0.14",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.8.2",
"typescript-eslint": "^8.26.1",
"typescript-eslint": "^8.27.0",
"vite": "^6.2.2"
}
}

View File

@ -20,7 +20,6 @@
"@codemirror/autocomplete": "^6.18.6",
"@codemirror/basic-setup": "^0.20.0",
"@codemirror/commands": "^6.8.0",
"@codemirror/history": "^0.19.2",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-javascript": "^6.2.3",

View File

@ -5,10 +5,10 @@ import { html } from '@codemirror/lang-html';
import { css } from '@codemirror/lang-css';
import { json } from '@codemirror/lang-json';
import { yaml } from '@codemirror/lang-yaml';
import { history } from '@codemirror/history';
import { history } from '@codemirror/commands';
import { vscodeLight } from '@uiw/codemirror-theme-vscode';
import { formatKeymap } from './modules/keymap';
import { Compartment, EditorState, Extension } from '@codemirror/state';
import { Compartment, Extension } from '@codemirror/state';
import { defaultKeymap } from '@codemirror/commands';
import { autocompletion, Completion } from '@codemirror/autocomplete';
import { getFileType } from './utils/get-file-type';
@ -58,7 +58,7 @@ export class BaseEditor {
vscodeLight,
formatKeymap,
keymap.of(defaultKeymap), //
// history(),
history(),
];
if (this.autoComplete?.open) {
extensions.push(

View File

@ -1,13 +1,13 @@
import { Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Button } from '@mui/material';
import { useRef, useState } from 'react';
import type { ModalFuncProps } from 'antd';
export const Confirm = ({
open,
onClose,
title,
content,
onConfirm,
confirmText = '确认',
okText = '确认',
cancelText = '取消',
}: {
open: boolean;
@ -15,11 +15,15 @@ export const Confirm = ({
title: string;
content: string;
onConfirm?: () => void;
confirmText?: string;
okText?: string;
cancelText?: string;
}) => {
return (
<Dialog open={open} onClose={onClose} aria-labelledby='alert-dialog-title' aria-describedby='alert-dialog-description'>
<Dialog
open={open}
onClose={onClose}
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'>
<DialogTitle id='alert-dialog-title' className='text-secondary min-w-[300px]'>
{title}
</DialogTitle>
@ -31,7 +35,7 @@ export const Confirm = ({
{cancelText || '取消'}
</Button>
<Button onClick={onConfirm} variant='contained' color='primary' autoFocus>
{confirmText || '确认'}
{okText || '确认'}
</Button>
</DialogActions>
</Dialog>
@ -39,26 +43,48 @@ export const Confirm = ({
};
type Fn = () => void;
export const useConfirm = () => {
export const useModal = () => {
const [open, setOpen] = useState(false);
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const fns = useRef<{
onConfirm: Fn;
onCancel: Fn;
confirmText: string;
okText: string;
cancelText: string;
}>({
onConfirm: () => {},
onCancel: () => {},
confirmText: '确认',
okText: '确认',
cancelText: '取消',
});
return {
contextHolder: (
const modal = {
confirm: (props: ModalFuncProps) => {
setOpen(true);
setTitle(props.title as string);
setContent(props.content as string);
fns.current.onConfirm = async () => {
const isClose = await props.onOk?.();
if (!isClose) {
setOpen(false);
}
};
fns.current.onCancel = async () => {
await props.onCancel?.();
setOpen(false);
};
fns.current.okText = props.okText as string;
fns.current.cancelText = props.cancelText as string;
},
cancel: () => {
setOpen(false);
fns.current.onCancel();
},
};
const contextHolder = (
<Confirm
open={open}
confirmText={fns.current.confirmText}
okText={fns.current.okText}
cancelText={fns.current.cancelText}
onClose={() => {
setOpen(false);
@ -68,24 +94,6 @@ export const useConfirm = () => {
content={content}
onConfirm={fns.current.onConfirm}
/>
),
confirm: (
title: string,
content: string,
opts?: {
onConfirm: () => void;
confirmText?: string;
cancelText?: string;
onCancel?: () => void;
},
) => {
setOpen(true);
setTitle(title);
setContent(content);
fns.current.onConfirm = opts?.onConfirm || (() => {});
fns.current.onCancel = opts?.onCancel || (() => {});
fns.current.confirmText = opts?.confirmText || '确认';
fns.current.cancelText = opts?.cancelText || '取消';
},
};
);
return [modal, contextHolder] as [typeof modal, React.ReactNode];
};

View File

@ -0,0 +1,47 @@
import { Fragment, useEffect, useState } from 'react';
import Autocomplete from '@mui/material/Autocomplete';
import { TextField, Chip } from '@mui/material';
type TagsInputProps = {
value: string[];
onChange: (value: string[]) => void;
placeholder?: string;
label?: string;
};
export const TagsInput = ({ value, onChange, placeholder = 'Add a tag', label = 'Tags' }: TagsInputProps) => {
const [tags, setTags] = useState<string[]>(value);
useEffect(() => {
setTags(value);
}, [value]);
const randomid = () => {
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
};
return (
<Autocomplete
multiple
freeSolo
options={[]}
value={tags}
onChange={(event, newValue) => {
// setTags(newValue as string[]);
onChange(newValue as string[]);
}}
renderTags={(value: string[], getTagProps) => {
const id = randomid();
const com = value.map((option: string, index: number) => (
<Chip
variant='outlined'
sx={{
borderColor: 'primary.main',
}}
label={option}
{...getTagProps({ index })}
key={`${id}-${index}`}
/>
));
return <Fragment key={id}>{com}</Fragment>;
}}
renderInput={(params) => <TextField {...params} variant='outlined' label={label} placeholder={placeholder} />}
/>
);
};

View File

@ -0,0 +1,18 @@
import { MenuItem, Select as MuiSelect, SelectProps as MuiSelectProps } from '@mui/material';
type SelectProps = {
options?: { label: string; value: string }[];
} & MuiSelectProps;
export const Select = (props: SelectProps) => {
const { options, ...rest } = props;
return (
<MuiSelect {...rest}>
{options?.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</MuiSelect>
);
};

View File

@ -58,7 +58,7 @@ export const themeOptions: ThemeOptions = {
// paper: '#f5f5f5', // 设置纸张背景颜色
},
error: {
main: red[500],
main: red[500], // 设置错误颜色 "#f44336"
},
},
shadows: generateShadows('rgba(255, 193, 7, 0.2)'),
@ -94,6 +94,14 @@ export const themeOptions: ThemeOptions = {
},
},
MuiTextField: {
defaultProps: {
fullWidth: true,
slotProps: {
inputLabel: {
shrink: true,
},
},
},
styleOverrides: {
root: {
'& .MuiOutlinedInput-root': {
@ -145,6 +153,29 @@ export const themeOptions: ThemeOptions = {
},
},
},
MuiFormControlLabel: {
defaultProps: {
labelPlacement: 'top',
sx: {
alignItems: 'flex-start',
'& .MuiFormControlLabel-label': {
textAlign: 'left',
width: '100%',
},
'& .MuiFormControlLabel-root': {
width: '100%',
},
'& .MuiInputBase-root': {
width: '100%',
},
},
},
styleOverrides: {
root: {
color: amber[600],
},
},
},
},
};

View File

@ -1,12 +1,14 @@
import { useResourceStore } from '@kevisual/resources/pages/store/resource';
import { useResourceFileStore } from '@kevisual/resources/pages/store/resource-file';
import { Box, Divider, Drawer, Tab, Tabs } from '@mui/material';
import { Box, Button, Divider, Drawer, Tab, Tabs } from '@mui/material';
import { useMemo, useState } from 'react';
import { QuickValues, QuickTabs } from './QuickTabs';
import { Delete, Trash } from 'lucide-react';
import { InitProvider } from '../../App';
export const FileDrawer = () => {
const { prefix } = useResourceStore();
const { resource, openDrawer, setOpenDrawer } = useResourceFileStore();
const { prefix, getList } = useResourceStore();
const { resource, openDrawer, setOpenDrawer, deleteFile } = useResourceFileStore();
const [tab, setTab] = useState<string>(QuickValues[0]);
const quickCom = useMemo(() => {
return QuickTabs.find((item) => item.value === tab)?.component;
@ -31,10 +33,26 @@ export const FileDrawer = () => {
style={{
zIndex: 1000,
}}>
<div className='p-4 w-[400px] max-w-[90%] overflow-hidden h-full sm:w-[600px]'>
<div className='p-4 overflow-hidden h-full sm:w-[600px]'>
<div style={{ height: '140px' }}>
<h2 className='text-2xl font-bold truncate py-2 pb-6 '>
{resource?.name ? resource.name.replace(prefix, '') : resource?.prefix?.replace(prefix, '')}
<h2 className='text-2xl font-bold py-2 pb-6 flex '>
<div className='grow truncate'>{resource?.name ? resource.name.replace(prefix, '') : resource?.prefix?.replace(prefix, '')}</div>
<Button
sx={{ ml: 2 }}
color='primary'
size='small'
onClick={() => {
if (resource) {
deleteFile(resource, {
onSuccess: () => {
getList();
setOpenDrawer(false);
},
});
}
}}>
<Trash />
</Button>
</h2>
<Divider />
<Box sx={{ borderBottom: 1, mt: 2, borderColor: 'divider' }}>
@ -58,3 +76,11 @@ export const FileDrawer = () => {
</>
);
};
export const FileDrawerApp = () => {
return (
<InitProvider>
<FileDrawer />
</InitProvider>
);
};

View File

@ -1,13 +1,13 @@
import { useResourceFileStore } from '@kevisual/resources/pages/store/resource-file';
import { FormControlLabel, Box, TextField, Button, IconButton, ButtonGroup, Tooltip, Select, MenuItem, Typography, FormGroup } from '@mui/material';
import { FormControlLabel, Box, ButtonGroup, Tooltip, Typography } from '@mui/material';
import { IconButton } from '@kevisual/center-components/button/index.tsx';
import { Info, Plus, Save, Share, Shuffle, Trash } from 'lucide-react';
import { useState, useEffect, useMemo } from 'react';
import { useState, useEffect } from 'react';
import { toast } from 'react-toastify';
import { create } from 'zustand';
import { uniq } from 'lodash-es';
import { DatePicker } from './DatePicker';
import { SelectPicker } from './SelectPicker';
import dayjs from 'dayjs';
import { DialogKey } from './DialogKey';
import { keysTips, KeyParse } from '../../modules/key-parse';
import { KeyShareSelect, KeyTextField } from '../../modules/PermissionManager';
@ -139,7 +139,7 @@ export const MetaForm = () => {
} else {
_formData[key] = value;
}
setFormData(_formData);
setFormData({ ..._formData });
};
const deleteMeta = (key: string) => {
setKeys(keys.filter((item) => item !== key));
@ -166,13 +166,14 @@ export const MetaForm = () => {
<Box className='sticky top-0 z-10 pointer-events-none'>
<div className='flex justify-end mr-20'>
<div className=' pointer-events-auto '>
<ButtonGroup className='bg-white' variant='contained' sx={{ color: 'white' }}>
<ButtonGroup
variant='contained'
sx={{
mt: 1,
backgroundColor: 'primary.main',
}}>
{btnList.map((item) => {
const icon = (
<IconButton color='secondary' onClick={item.onClick}>
{item.icon}
</IconButton>
);
const icon = <IconButton onClick={item.onClick}>{item.icon}</IconButton>;
if (item.tooltip) {
return (
<Tooltip key={item.key} title={item.tooltip} placement='top' arrow>
@ -212,7 +213,14 @@ export const MetaForm = () => {
</Tooltip>
)}
</div>
<IconButton color='error' onClick={() => deleteMeta(key)}>
<IconButton
variant='text'
sx={{
color: 'primay.main',
}}
color='error'
size='small'
onClick={() => deleteMeta(key)}>
<Trash />
</IconButton>
</div>
@ -241,5 +249,3 @@ export const MetaForm = () => {
</div>
);
};

View File

@ -6,12 +6,12 @@ import { getIcon } from '../FileIcon';
import { Download, Trash } from 'lucide-react';
import clsx from 'clsx';
import { useResourceFileStore } from '@kevisual/resources/pages/store/resource-file';
import { useConfirm } from '@kevisual/center-components/modal/Confirm.tsx';
import { useModal } from '@kevisual/center-components/modal/Confirm.tsx';
export const FileTable = () => {
const { list, prefix, download, onOpenPrefix, deleteFile } = useResourceStore();
const { setOpenDrawer, setPrefix } = useResourceFileStore();
const { confirm, contextHolder } = useConfirm();
const { list, prefix, download, onOpenPrefix, getList } = useResourceStore();
const { setOpenDrawer, setPrefix, deleteFile } = useResourceFileStore();
const [modal, contextHolder] = useModal();
return (
<>
{contextHolder}
@ -95,9 +95,15 @@ export const FileTable = () => {
className='ml-2!'
onClick={(e) => {
e.stopPropagation();
confirm('删除文件', '确定删除该文件吗?', {
onConfirm: () => {
deleteFile(row);
modal.confirm({
title: '删除文件',
content: '确定删除该文件吗?',
onOk: () => {
deleteFile(row, {
onSuccess: () => {
getList();
},
});
},
});
}}

View File

@ -4,6 +4,8 @@ import { FormControlLabel, TextField, Select, MenuItem, FormGroup, Tooltip } fro
import { DatePicker } from '../draw/modules/DatePicker';
import { SelectPicker } from '../draw/modules/SelectPicker';
import { HelpCircle } from 'lucide-react';
import clsx from 'clsx';
import { useTranslation } from 'react-i18next';
export const KeyShareSelect = ({ name, value, onChange }: { name: string; value: string; onChange?: (value: string) => void }) => {
return (
<Select
@ -34,8 +36,7 @@ export const KeyTextField = ({ name, value, onChange }: { name: string; value: s
variant='outlined'
size='small'
name={name}
defaultValue={value}
// value={formData[key] || ''}
value={value}
onChange={(e) => onChange?.(e.target.value)}
sx={{
width: '100%',
@ -48,8 +49,10 @@ export const KeyTextField = ({ name, value, onChange }: { name: string; value: s
type PermissionManagerProps = {
value: Record<string, any>;
onChange: (value: Record<string, any>) => void;
className?: string;
};
export const PermissionManager = ({ value, onChange }: PermissionManagerProps) => {
export const PermissionManager = ({ value, onChange, className }: PermissionManagerProps) => {
const { t, i18n } = useTranslation();
const [formData, setFormData] = useState<any>(value);
const [keys, setKeys] = useState<any>([]);
useEffect(() => {
@ -80,26 +83,19 @@ export const PermissionManager = ({ value, onChange }: PermissionManagerProps) =
onChange(KeyParse.stringify(newFormData));
}
};
const tips = getTips('share', i18n.language);
return (
<form className='w-[400px] flex flex-col gap-2'>
<form className={clsx('flex flex-col gap-2', className)}>
<FormControlLabel
labelPlacement='top'
control={<KeyShareSelect name='share' value={formData?.share} onChange={(value) => onChangeValue('share', value)} />}
label={
<div className='flex items-center gap-1'>
Share
<Tooltip title={getTips('share')}>
{t('Share')}
<Tooltip title={tips}>
<HelpCircle size={16} />
</Tooltip>
</div>
}
sx={{
alignItems: 'flex-start',
'& .MuiFormControlLabel-label': {
textAlign: 'left',
width: '100%',
},
}}
/>
{keys.map((item: any) => {
let control: React.ReactNode | null = null;

View File

@ -1,6 +1,13 @@
import dayjs from 'dayjs';
export const getTips = (key: string) => {
return keysTips.find((item) => item.key === key)?.tips;
export const getTips = (key: string, lang?: string) => {
const tip = keysTips.find((item) => item.key === key);
if (tip) {
if (lang === 'en') {
return tip.enTips;
}
return tip.tips;
}
return '';
};
export const keysTips = [
{
@ -10,26 +17,35 @@ export const keysTips = [
2. 访
3. 访\n
访 访`,
enTips: `1. Set public to directly access
2. Set protected to access after login
3. Set private to access only yourself.
Protected can set a password and set the username for access. After switching the shared state, you need to reset the password and username. If not set, it defaults to only being accessible to yourself.`,
},
{
key: 'content-type',
tips: `内容类型,设置文件的内容类型。默认不要修改。`,
enTips: `Content type, set the content type of the file. Default do not modify.`,
},
{
key: 'app-source',
tips: `应用来源,上传方式。默认不要修改。`,
enTips: `App source, upload method. Default do not modify.`,
},
{
key: 'cache-control',
tips: `缓存控制,设置文件的缓存控制。默认不要修改。`,
enTips: `Cache control, set the cache control of the file. Default do not modify.`,
},
{
key: 'password',
tips: `密码,设置文件的密码。不设置默认是所有人都可以访问。`,
enTips: `Password, set the password of the file. If not set, it defaults to everyone can access.`,
},
{
key: 'usernames',
tips: `用户名,设置文件的用户名。不设置默认是所有人都可以访问。`,
enTips: `Username, set the username of the file. If not set, it defaults to everyone can access.`,
parse: (value: string) => {
if (!value) {
return [];
@ -46,6 +62,7 @@ export const keysTips = [
{
key: 'expiration-time',
tips: `过期时间,设置文件的过期时间。不设置默认是永久。`,
enTips: `Expiration time, set the expiration time of the file. If not set, it defaults to permanent.`,
parse: (value: Date) => {
if (!value) {
return null;

View File

@ -10,8 +10,15 @@ interface ResourceFileStore {
setOpenDrawer: (openDrawer: boolean) => void;
prefix: string;
setPrefix: (prefix: string, replace?: string) => void;
/**
* prefix
* @returns
*/
getStatFile: () => Promise<any>;
updateMeta: (metadata: any) => Promise<any>;
deleteFile: (resource: Resource, opts?: { onSuccess?: (res: any) => void }) => Promise<void>;
once: ((data: any) => any) | null;
setOnce: (data: any) => void;
}
export const useResourceFileStore = create<ResourceFileStore>((set, get) => ({
@ -45,10 +52,35 @@ export const useResourceFileStore = create<ResourceFileStore>((set, get) => ({
},
});
if (res.code === 200) {
// set({ resource: { ...res.data, name: resource?.name } });
toast.success('Update metadata success');
getStatFile();
} else {
toast.error(res.message || '更新元数据失败');
}
},
deleteFile: async (resource: Resource, opts?: { onSuccess?: (res: any) => void }) => {
const { once, setOnce } = get();
const name = resource.name;
if (!name) {
toast.error('Resource is not a file');
return;
}
const res = await query.post({
path: 'file',
key: 'delete',
data: {
prefix: name,
},
});
if (res.code === 200) {
toast.success('Delete file success');
opts?.onSuccess?.(res);
once?.(res);
setOnce(null);
} else {
toast.error(res.message || 'Request failed');
}
},
once: null,
setOnce: (data: any) => set({ once: data }),
}));

View File

@ -3,30 +3,67 @@ import { useDropzone } from 'react-dropzone';
import { uploadFiles } from './utils/upload';
import { FileText, CloudUpload as UploadIcon } from 'lucide-react';
import { uploadFileChunked } from './utils/upload-chunk';
export const UploadButton = (props: { prefix?: string; onUpload?: (res: any) => void; hasDirectory?: boolean; uploadDirectory?: boolean }) => {
import { filterFiles } from './utils/filter-files';
type UploadButtonProps = {
/**
*
*/
directory?: string;
/**
* key
*/
appKey?: string;
/**
*
*/
version?: string;
/**
*
*/
username?: string;
/**
*
*/
onUpload?: (res: any) => void;
/**
*
*/
uploadDirectory?: boolean;
/**
*
*/
onlyIcon?: boolean;
/**
*
*/
icon?: React.ReactNode;
};
/**
*
* @param props
* @returns
*/
export const UploadButton = (props: UploadButtonProps) => {
const { onlyIcon = false, icon } = props;
const { appKey, version, username, directory } = props;
const onDrop = async (acceptedFiles) => {
console.log(acceptedFiles);
acceptedFiles = filterFiles(acceptedFiles);
if (acceptedFiles.length > 1) {
const res = await uploadFiles(acceptedFiles, { directory: props.prefix });
const res = await uploadFiles(acceptedFiles, { directory, appKey, version, username });
console.log('uploadFiles res', res);
props.onUpload?.(res);
} else if (acceptedFiles.length === 1) {
const res = await uploadFileChunked(acceptedFiles[0], { directory: props.prefix });
const res = await uploadFileChunked(acceptedFiles[0], { directory, appKey, version, username });
console.log('uploadFiles res', res);
props.onUpload?.(res);
}
};
const { getRootProps, getInputProps } = useDropzone({ onDrop });
return (
<Box {...getRootProps()}>
<Button
color='primary'
sx={{
minWidth: 'unset',
padding: '2px',
}}>
<UploadIcon />
</Button>
const uploadCom = (
<div {...getRootProps()}>
{icon || <UploadIcon />}
<input
type='file'
style={{ display: 'none' }}
@ -35,12 +72,27 @@ export const UploadButton = (props: { prefix?: string; onUpload?: (res: any) =>
webkitdirectory={props.uploadDirectory ? 'true' : undefined}
mozdirectory={props.uploadDirectory ? 'true' : undefined}
/>
</div>
);
if (onlyIcon) {
return uploadCom;
}
return (
<Box>
<Button
color='primary'
sx={{
minWidth: 'unset',
padding: '2px',
}}>
{uploadCom}
</Button>
</Box>
);
};
export const Upload = ({ uploadDirectory = false }: { uploadDirectory?: boolean }) => {
const onDrop = async (acceptedFiles) => {
console.log(acceptedFiles);
acceptedFiles = filterFiles(acceptedFiles);
if (acceptedFiles.length > 1) {
const res = await uploadFiles(acceptedFiles, {});
console.log('uploadFiles res', res);

View File

@ -21,12 +21,8 @@ const getFileType = (extension: string) => {
return 'image/gif';
case 'svg':
return 'image/svg+xml';
case 'ico':
return 'image/x-icon';
case 'webp':
return 'image/webp';
case 'gif':
return 'image/gif';
case 'ico':
return 'image/x-icon';
default:

View File

@ -0,0 +1,23 @@
/**
* , .DS_Store, node_modules, . __开头的文件
* @param files
* @returns
*/
export const filterFiles = (files: File[]) => {
files = files.filter((file) => {
if (file.webkitRelativePath.startsWith('__MACOSX')) {
return false;
}
// 过滤node_modules
if (file.webkitRelativePath.includes('node_modules')) {
return false;
}
// 过滤文件 .DS_Store
if (file.name === '.DS_Store') {
return false;
}
// 过滤以.开头的文件
return !file.name.startsWith('.');
});
return files;
};

View File

@ -18,6 +18,7 @@ export const uploadFileChunked = async (file: File, opts: ConvertOpts) => {
return new Promise(async (resolve, reject) => {
const token = localStorage.getItem('token');
if (!token) {
console.log('uploadFileChunked token', token);
toastLogin();
return;
}
@ -33,6 +34,7 @@ export const uploadFileChunked = async (file: File, opts: ConvertOpts) => {
searchParams.set('public', 'true');
}
const eventSource = new EventSource('/api/s1/events?' + searchParams.toString());
let isError = false;
// 监听服务器推送的进度更新
eventSource.onmessage = function (event) {
console.log('Progress update:', event.data);
@ -55,6 +57,7 @@ export const uploadFileChunked = async (file: File, opts: ConvertOpts) => {
};
eventSource.onerror = function (event) {
console.log('eventSource.onerror', event);
isError = true;
reject(event);
};
@ -91,6 +94,15 @@ export const uploadFileChunked = async (file: File, opts: ConvertOpts) => {
},
}).then((response) => response.json());
fetch('/api/s1/events/close?taskId=' + taskId);
if (res?.code !== 200) {
toast.error('上传失败');
isError = true;
NProgress.done();
eventSource.close();
toast.dismiss(load);
reject(new Error(res?.message || '上传失败'));
return;
}
if (isLast) {
NProgress.done();
eventSource.close();

View File

@ -3,7 +3,6 @@ import 'nprogress/nprogress.css';
import { toast } from 'react-toastify';
import { nanoid } from 'nanoid';
import { toastLogin } from '@kevisual/resources/pages/message/ToastLogin';
type ConvertOpts = {
appKey?: string;
version?: string;
@ -39,6 +38,7 @@ export const uploadFiles = async (files: File[], opts: ConvertOpts) => {
}
const token = localStorage.getItem('token');
if (!token) {
console.log('uploadFiles token', token);
toastLogin();
return;
}

583
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -41,5 +41,13 @@
"User List": "User List",
"Switch to Org": "Switch to Org",
"Login": "Login",
"uploadDirectory": "Upload Directory"
"uploadDirectory": "Upload Directory",
"refresh": "Refresh",
"upload": "Upload",
"app": {
"domain": "Domain",
"version": "Version",
"runtime": "Can run environment"
},
"Share": "Share"
}

View File

@ -41,5 +41,13 @@
"User List": "用户列表",
"Switch to Org": "切换组织",
"Login": "登录",
"uploadDirectory": "上传文件夹"
"uploadDirectory": "上传文件夹",
"refresh": "刷新",
"upload": "上传",
"app": {
"domain": "访问域名",
"version": "版本",
"runtime": "可以运行的环境"
},
"Share": "分享"
}

View File

@ -10,19 +10,30 @@ import { Redirect } from './modules/Redirect';
import { CustomThemeProvider } from '@kevisual/center-components/theme/index.tsx';
import { useTheme } from '@mui/material/styles';
import { ToastContainer } from 'react-toastify';
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
import 'dayjs/locale/en';
import zhCN from 'antd/locale/zh_CN';
import enUS from 'antd/locale/en_US';
import ConfigProvider from 'antd/es/config-provider';
import { useTranslation } from 'react-i18next';
import { useEffect, useState } from 'react';
const AntProvider = ({ children }: { children: React.ReactNode }) => {
const theme = useTheme();
const primaryColor = theme.palette.primary.main;
const secondaryColor = theme.palette.secondary.main;
const { i18n } = useTranslation();
const [locale, setLocale] = useState(zhCN);
useEffect(() => {
if (i18n.language === 'en') {
setLocale(enUS);
} else {
setLocale(zhCN);
}
}, [i18n.language]);
return (
<ConfigProvider
locale={zhCN}
locale={locale}
theme={{
token: {
colorPrimary: primaryColor,
@ -43,7 +54,7 @@ const AntProvider = ({ children }: { children: React.ReactNode }) => {
},
Tooltip: {
zIndexPopupBase: 2000,
}
},
},
}}>
{children}

View File

@ -57,8 +57,9 @@ h3 {
}
}
.cm-editor {
@apply h-full;
}
#for-message {
z-index: 9999 !important;
}

View File

@ -1,3 +1,2 @@
import { message } from '@kevisual/system-ui/dist/message';
export { message };

View File

@ -2,8 +2,14 @@ import { useNavigation, useParams } from 'react-router';
import { useAppVersionStore } from '../store';
import { useShallow } from 'zustand/react/shallow';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Form, Input, Modal, Tooltip } from 'antd';
import { CloudUploadOutlined, DeleteOutlined, EditOutlined, FileOutlined, LeftOutlined, LinkOutlined, PlusOutlined } from '@ant-design/icons';
import CloudUploadOutlined from '@ant-design/icons/CloudUploadOutlined';
import DeleteOutlined from '@ant-design/icons/DeleteOutlined';
import FileOutlined from '@ant-design/icons/FileOutlined';
import LeftOutlined from '@ant-design/icons/LeftOutlined';
import LinkOutlined from '@ant-design/icons/LinkOutlined';
import PlusOutlined from '@ant-design/icons/PlusOutlined';
import { useModal } from '@kevisual/center-components/modal/Confirm.tsx';
import { Tooltip } from '@mui/material';
import { isObjectNull } from '@/utils/is-null';
import { FileUpload } from '../modules/FileUpload';
import clsx from 'clsx';
@ -13,9 +19,13 @@ import { Button } from '@mui/material';
import { Dialog, DialogContent, DialogTitle, ButtonGroup } from '@mui/material';
import { useTranslation } from 'react-i18next';
import { IconButton } from '@kevisual/center-components/button/index.tsx';
import { useForm, Controller } from 'react-hook-form';
import { TextField } from '@mui/material';
import { pick } from 'lodash-es';
const FormModal = () => {
const { t } = useTranslation();
const [form] = Form.useForm();
const { control, handleSubmit, reset } = useForm();
const containerStore = useAppVersionStore(
useShallow((state) => {
return {
@ -26,62 +36,56 @@ const FormModal = () => {
};
}),
);
useEffect(() => {
const open = containerStore.showEdit;
if (open) {
if (open) {
const isNull = isObjectNull(containerStore.formData);
if (isNull) {
form.setFieldsValue({});
} else form.setFieldsValue(containerStore.formData);
reset({});
} else {
reset(containerStore.formData);
}
}
}, [containerStore.showEdit]);
const onFinish = async (values: any) => {
containerStore.updateData(values);
const pickValues = pick(values, ['id', 'key', 'version']);
containerStore.updateData(pickValues);
};
const onClose = () => {
containerStore.setShowEdit(false);
form.resetFields();
reset();
};
const isEdit = containerStore.formData.id;
return (
<Modal
title={isEdit ? 'Edit' : 'Add'}
<Dialog
open={containerStore.showEdit}
onClose={() => containerStore.setShowEdit(false)}
destroyOnClose
footer={false}
width={800}
onCancel={onClose}>
<Form
form={form}
onFinish={onFinish}
labelCol={{
span: 4,
}}
wrapperCol={{
span: 20,
sx={{
'& .MuiDialog-paper': {
width: '800px',
},
}}>
<Form.Item name='id' hidden>
<Input />
</Form.Item>
<Form.Item name='key' label='key'>
<Input disabled />
</Form.Item>
<Form.Item name='version' label='version'>
<Input />
</Form.Item>
<Form.Item label=' ' colon={false}>
<DialogTitle>{isEdit ? 'Edit' : 'Add'}</DialogTitle>
<DialogContent>
<form className='flex flex-col gap-6' onSubmit={handleSubmit(onFinish)}>
<Controller name='key' control={control} defaultValue='' render={({ field }) => <TextField label='key' {...field} disabled />} />
<Controller name='version' control={control} defaultValue='' render={({ field }) => <TextField label='version' {...field} />} />
<div>
<Button type='submit' variant='contained' color='primary'>
{t('submit')}
</Button>
<Button className='ml-2' onClick={onClose}>
{t('cancel')}
</Button>
</Form.Item>
</Form>
</Modal>
</div>
</form>
</DialogContent>
</Dialog>
);
};
@ -105,7 +109,7 @@ export const AppVersionList = () => {
}),
);
const navigate = useNewNavigate();
const [modal, contextHolder] = Modal.useModal();
const [modal, contextHolder] = useModal();
const [isUpload, setIsUpload] = useState(false);
useEffect(() => {
// fetch app version list

View File

@ -1,7 +1,8 @@
import { useShallow } from 'zustand/react/shallow';
import { useUserAppStore } from '../store';
import { useEffect, useState } from 'react';
import { Form, Input, Modal, Select, Switch } from 'antd';
import { useModal } from '@kevisual/center-components/modal/Confirm.tsx';
import DeleteOutlined from '@ant-design/icons/DeleteOutlined';
import EditOutlined from '@ant-design/icons/EditOutlined';
import LinkOutlined from '@ant-design/icons/LinkOutlined';
@ -9,26 +10,43 @@ import PlusOutlined from '@ant-design/icons/PlusOutlined';
import UnorderedListOutlined from '@ant-design/icons/UnorderedListOutlined';
import CodeOutlined from '@ant-design/icons/CodeOutlined';
import ShareAltOutlined from '@ant-design/icons/ShareAltOutlined';
import { FormControlLabel, Switch } from '@mui/material';
import { isObjectNull } from '@/utils/is-null';
import { useNewNavigate } from '@/modules';
import { DialogActions, Tooltip } from '@mui/material';
import { marked } from 'marked';
import clsx from 'clsx';
import { IconButton } from '@kevisual/center-components/button/index.tsx';
import { Select } from '@kevisual/center-components/select/index.tsx';
import { iText } from '@kevisual/resources/index.ts';
import { PermissionManager } from '@kevisual/resources/pages/file/modules/PermissionManager.tsx';
import { Button } from '@mui/material';
import { message } from '@/modules/message';
import { Dialog, DialogContent, DialogTitle, ButtonGroup } from '@mui/material';
import { useTranslation } from 'react-i18next';
import { TextField, InputAdornment } from '@mui/material';
import { useForm, Controller } from 'react-hook-form';
import { pick } from 'lodash-es';
const FormModal = () => {
const [form] = Form.useForm();
const defaultValues = {
id: '',
title: '',
domain: '',
key: '',
description: '',
proxy: true,
status: 'running',
};
const { control, handleSubmit, reset } = useForm({
defaultValues,
});
const containerStore = useUserAppStore(
useShallow((state) => {
return {
showEdit: state.showEdit,
setShowEdit: state.setShowEdit,
formData: state.formData,
userApp: state.userApp,
updateData: state.updateData,
};
}),
@ -36,121 +54,113 @@ const FormModal = () => {
useEffect(() => {
const open = containerStore.showEdit;
if (open) {
if (open) {
const isNull = isObjectNull(containerStore.formData);
const isNull = isObjectNull(containerStore.userApp);
console.log('isNull', containerStore.userApp);
if (isNull) {
form.setFieldsValue({});
} else form.setFieldsValue(containerStore.formData);
reset(defaultValues);
} else {
reset(containerStore.userApp);
}
}
}, [containerStore.showEdit]);
}, [containerStore.showEdit, containerStore.userApp]);
const onFinish = async (values: any) => {
containerStore.updateData(values);
const pickValues = pick(values, ['id', 'title', 'domain', 'key', 'description', 'proxy', 'status']);
containerStore.updateData(pickValues);
};
const onClose = () => {
containerStore.setShowEdit(false);
form.resetFields();
reset();
};
const isEdit = containerStore.formData.id;
const isEdit = containerStore?.userApp?.id;
return (
<Dialog
title={isEdit ? 'Edit' : 'Add'}
open={containerStore.showEdit}
onClose={() => containerStore.setShowEdit(false)}
sx={{
'& .MuiDialog-paper': {
width: '800px',
width: '1000px',
},
}}>
<DialogTitle>{isEdit ? 'Edit' : 'Add'}</DialogTitle>
<DialogContent>
<Form
form={form}
onFinish={onFinish}
initialValues={{
proxy: true,
}}
labelCol={{
span: 4,
}}
wrapperCol={{
span: 20,
}}>
<Form.Item name='id' hidden>
<Input />
</Form.Item>
<Form.Item name='title' label='title'>
<Input />
</Form.Item>
<Form.Item name='domain' label='domain' tooltip='域名自定义绑定'>
<Input />
</Form.Item>
<Form.Item name='key' label='key'>
<Input />
</Form.Item>
<Form.Item name='description' label='description'>
<Input.TextArea rows={4} />
</Form.Item>
<Form.Item name='proxy' label='proxy' tooltip='设置为true则后端直接代理请求minio服务进行转发不会缓存下载到服务器。'>
<Switch />
</Form.Item>
<Form.Item name='status' label='status'>
<form className='flex flex-col gap-4 pt-2' onSubmit={handleSubmit(onFinish)}>
<Controller name='title' control={control} render={({ field }) => <TextField {...field} label='title' fullWidth />} />
<Controller
name='domain'
control={control}
render={({ field }) => <TextField {...field} label='domain' variant='outlined' helperText='域名自定义绑定' />}
/>
<Controller name='key' control={control} render={({ field }) => <TextField {...field} label='key' fullWidth />} />
<Controller name='description' control={control} render={({ field }) => <TextField {...field} label='description' multiline rows={4} fullWidth />} />
<Controller name='proxy' control={control} render={({ field }) => <Switch {...field} checked={field.value} />} />
<Controller
name='status'
control={control}
render={({ field }) => (
<Select
{...field}
sx={{
width: '100%',
}}
options={[
{ label: 'Running', value: 'running' },
{ label: 'Stop', value: 'stop' },
]}
/>
</Form.Item>
<Form.Item label=' ' colon={false}>
)}
/>
<div>
<Button type='submit'></Button>
<Button className='ml-2' type='reset' onClick={onClose}>
</Button>
</Form.Item>
</Form>
</div>
</form>
</DialogContent>
</Dialog>
);
};
const ShareModal = () => {
const [form] = Form.useForm();
const [permission, setPermission] = useState<any>(null);
const [runtime, setRuntime] = useState<string[]>([]);
const containerStore = useUserAppStore(
useShallow((state) => {
return {
showEdit: state.showShareEdit,
setShowEdit: state.setShowShareEdit,
formData: state.formData,
updateData: state.updateData,
userApp: state.userApp,
};
}),
);
useEffect(() => {
const open = containerStore.showEdit;
if (open) {
// form.setFieldsValue(containerStore.formData);
const permission = containerStore.formData?.data?.permission || {};
const permission = containerStore.userApp?.data?.permission || {};
const runtime = containerStore.userApp?.data?.runtime || [];
if (isObjectNull(permission)) {
setPermission(null);
} else {
setPermission(permission);
}
setRuntime(runtime);
}
}, [containerStore.showEdit]);
}, [containerStore.showEdit, containerStore.userApp]);
const onFinish = async () => {
const values = {
...containerStore.formData,
id: containerStore.userApp.id,
data: {
permission,
runtime,
},
};
containerStore.updateData(values);
};
const onClose = () => {
containerStore.setShowEdit(false);
form.resetFields();
};
const { t } = useTranslation();
console.log('runtime', runtime);
return (
<Dialog
open={containerStore.showEdit}
@ -159,27 +169,52 @@ const ShareModal = () => {
}}>
<DialogTitle>{iText.share.title}</DialogTitle>
<DialogContent>
<div className='flex flex-col gap-2 w-[400px] '>
<PermissionManager
value={permission}
onChange={(value) => {
setPermission(value);
}}
/>
<FormControlLabel
label={t('app.runtime')}
control={
<Select
multiple
size='small'
value={runtime}
onChange={(e) => {
setRuntime(e.target.value as string[]);
}}
options={[
{
label: 'Node',
value: 'node',
},
{
label: 'Browser',
value: 'browser',
},
]}
/>
}
/>
</div>
</DialogContent>
<DialogActions>
<Button type='submit' variant='contained' onClick={onFinish}>
{t('Submit')}
</Button>
<Button className='ml-2' type='reset' onClick={onClose}>
{t('Cancel')}
</Button>
</DialogActions>
</DialogContent>
</Dialog>
);
};
export const List = () => {
const [modal, contextHolder] = Modal.useModal();
const [modal, contextHolder] = useModal();
const { t } = useTranslation();
const userAppStore = useUserAppStore(
useShallow((state) => {
return {
@ -190,6 +225,7 @@ export const List = () => {
setFormData: state.setFormData,
deleteData: state.deleteData,
setShowShareEdit: state.setShowShareEdit,
getUserApp: state.getUserApp,
};
}),
);
@ -206,6 +242,7 @@ export const List = () => {
padding: '8px',
}}
onClick={() => {
userAppStore.setFormData({});
userAppStore.setShowEdit(true);
}}>
<PlusOutlined />
@ -242,8 +279,14 @@ export const List = () => {
</div>
</div>
<div>
{item.domain && <div className='text-xs'>访: {item.domain}</div>}
<div className='text-xs'>version: {item.version}</div>
{item.domain && (
<div className='text-xs'>
{t('app.domain')}: {item.domain}
</div>
)}
<div className='text-xs'>
{t('app.version')}: {item.version}
</div>
<div className={clsx('text-sm border border-gray-200 p-2 max-h-[140px] scrollbar my-1', !hasDescription && 'hidden')}>
<div dangerouslySetInnerHTML={{ __html: content }}></div>
</div>
@ -273,6 +316,7 @@ export const List = () => {
<Tooltip title={iText.share.tips}>
<Button
onClick={() => {
userAppStore.getUserApp(item.id);
userAppStore.setFormData(item);
userAppStore.setShowShareEdit(true);
}}>
@ -294,7 +338,7 @@ export const List = () => {
if (DEV_SERVER) {
baseUri = 'http://localhost:3005';
}
const link = new URL(`/${item.user}/${item.key}`, baseUri);
const link = new URL(`/${item.user}/${item.key}/`, baseUri);
window.open(link.toString(), '_blank');
} else {
message.error('The app is not running');

View File

@ -14,6 +14,9 @@ type UserAppStore = {
deleteData: (id: string) => Promise<void>;
showShareEdit: boolean;
setShowShareEdit: (showShareEdit: boolean) => void;
userApp: any;
setUserApp: (userApp: any) => void;
getUserApp: (id: string) => Promise<void>;
};
export const useUserAppStore = create<UserAppStore>((set, get) => {
return {
@ -69,5 +72,20 @@ export const useUserAppStore = create<UserAppStore>((set, get) => {
},
showShareEdit: false,
setShowShareEdit: (showShareEdit) => set({ showShareEdit }),
userApp: {},
setUserApp: (userApp) => set({ userApp }),
getUserApp: async (id) => {
set({ userApp: null });
const res = await query.post({
path: 'user-app',
key: 'get',
id,
});
if (res.code === 200) {
set({ userApp: res.data });
} else {
message.error(res.message || 'Request failed');
}
},
};
});

View File

@ -1,9 +1,6 @@
import { Input, Modal, Select } from 'antd';
import { Fragment, Suspense, useEffect, useState } from 'react';
import { TextArea } from '../components/TextArea';
import { useContainerStore } from '../store';
import { useShallow } from 'zustand/react/shallow';
import { Form } from 'antd';
// import copy from 'copy-to-clipboard';
import { useNewNavigate } from '@/modules';
import { message } from '@/modules/message';
@ -19,10 +16,16 @@ import { isObjectNull } from '@/utils/is-null';
import { CardBlank } from '@/components/card/CardBlank';
import { Settings } from 'lucide-react';
import React from 'react';
import { useModal } from '@kevisual/center-components/modal/Confirm.tsx';
import { useForm, Controller } from 'react-hook-form';
import { TextField } from '@mui/material';
import { pick } from 'lodash-es';
import { useTranslation } from 'react-i18next';
import { TagsInput } from '@kevisual/center-components/select/TagsInput.tsx';
const DrawEdit = React.lazy(() => import('../module/DrawEdit'));
const FormModal = () => {
const [form] = Form.useForm();
const { control, handleSubmit, reset, setValue } = useForm();
const containerStore = useContainerStore(
useShallow((state) => {
return {
@ -33,70 +36,75 @@ const FormModal = () => {
};
}),
);
useEffect(() => {
const open = containerStore.showEdit;
if (open) {
if (open) {
const isNull = isObjectNull(containerStore.formData);
if (isNull) {
form.setFieldsValue({});
} else form.setFieldsValue(containerStore.formData);
reset({});
} else {
Object.keys(containerStore.formData).forEach((key) => {
setValue(key, containerStore.formData[key]);
});
}
}
}, [containerStore.showEdit]);
const onFinish = async (values: any) => {
containerStore.updateData(values);
return () => {
reset({});
};
}, [containerStore.showEdit]);
const onFinish = async (values: any) => {
const pickValues = pick(values, ['id', 'title', 'description', 'tags', 'code']);
containerStore.updateData(pickValues);
};
const onClose = () => {
containerStore.setShowEdit(false);
form.resetFields();
reset();
};
const isEdit = containerStore.formData.id;
const { t } = useTranslation();
return (
<Dialog open={containerStore.showEdit} onClose={() => containerStore.setShowEdit(false)}>
<Dialog open={containerStore.showEdit} onClose={onClose}>
<DialogTitle>{isEdit ? 'Edit' : 'Add'}</DialogTitle>
<DialogContent sx={{ padding: '20px', minWidth: '600px' }}>
<Form
form={form}
onFinish={onFinish}
labelCol={{
span: 4,
<form className='flex flex-col gap-6 pt-2' onSubmit={handleSubmit(onFinish)}>
<Controller name='title' control={control} defaultValue='' render={({ field }) => <TextField {...field} label='Title' fullWidth />} />
<Controller
name='description'
control={control}
defaultValue=''
render={({ field }) => <TextField {...field} label='Description' multiline rows={4} fullWidth />}
/>
<Controller
name='tags'
control={control}
defaultValue={[]}
render={({ field }) => {
return <TagsInput key={'tags'} label='Tags' placeholder='添加标签' value={field.value} onChange={(value) => field.onChange(value)} />;
}}
wrapperCol={{
span: 20,
}}>
<Form.Item name='id' hidden>
<Input />
</Form.Item>
<Form.Item name='title' label='title'>
<Input />
</Form.Item>
<Form.Item name='description' label='description'>
<Input.TextArea rows={4} />
</Form.Item>
<Form.Item name='tags' label='tags'>
<Select mode='tags' />
</Form.Item>
/>
{!isEdit && (
<Form.Item name='code' label='code'>
<TextArea />
</Form.Item>
<Controller name='code' control={control} defaultValue='' render={({ field }) => <TextField {...field} label='Code' multiline fullWidth />} />
)}
<Form.Item label=' ' colon={false}>
<div>
<Button variant='contained' type='submit'>
{t('Submit')}
</Button>
<Button className='ml-2' onClick={onClose}>
{t('Cancel')}
</Button>
</Form.Item>
</Form>
</div>
</form>
</DialogContent>
</Dialog>
);
};
const PublishFormModal = () => {
const [form] = Form.useForm();
const { control, handleSubmit, reset, setValue, getValues } = useForm();
const { t } = useTranslation();
const containerStore = useContainerStore(
useShallow((state) => {
return {
@ -107,20 +115,25 @@ const PublishFormModal = () => {
};
}),
);
useEffect(() => {
const open = containerStore.showEdit;
if (open) {
if (open) {
const isNull = isObjectNull(containerStore.formData);
if (isNull) {
form.setFieldsValue({});
} else form.setFieldsValue(containerStore.formData);
reset({});
} else {
Object.keys(containerStore.formData).forEach((key) => {
setValue(key, containerStore.formData[key]);
});
}
}
}, [containerStore.showEdit]);
const onFinish = async () => {
const values = form.getFieldsValue();
const containerRes = await containerStore.updateData(values, { closePublish: false });
const values = getValues();
const pickValues = pick(values, ['id', 'publish.key', 'publish.version', 'publish.fileName', 'publish.description']);
const containerRes = await containerStore.updateData(pickValues, { closePublish: false });
if (containerRes.code === 200) {
const code = containerRes.data?.code || '-';
const fileName = values['publish']?.['fileName'];
@ -139,12 +152,11 @@ const PublishFormModal = () => {
const key = values['publish']['key'];
const version = values['publish']['version'];
const file = toFile(code, directoryAndName.name);
const res = await uploadFileChunked(file, {
const res = (await uploadFileChunked(file, {
appKey: key,
version,
directory: directoryAndName.directory,
});
// @ts-ignore
})) as any;
if (res.code === 200) {
message.success('upload success');
} else {
@ -152,66 +164,67 @@ const PublishFormModal = () => {
}
}
};
const onUpdate = async () => {
const values = form.getFieldsValue();
const values = getValues();
containerStore.updateData(values);
};
const onClose = () => {
containerStore.setShowEdit(false);
form.resetFields();
reset();
};
return (
<Dialog open={containerStore.showEdit} onClose={() => containerStore.setShowEdit(false)}>
<Dialog open={containerStore.showEdit} onClose={onClose}>
<DialogTitle>Publish</DialogTitle>
<DialogContent sx={{ padding: '10px', minWidth: '600px' }}>
<Form
form={form}
onFinish={onFinish}
labelCol={{
span: 6,
}}
wrapperCol={{
span: 18,
}}>
<Form.Item name='id' hidden>
<Input />
</Form.Item>
<Form.Item name={['publish', 'key']} label='App key' required>
<Input />
</Form.Item>
<Form.Item name={['publish', 'version']} label='App Version' required>
<Input />
</Form.Item>
<Form.Item name={['publish', 'fileName']} label='Filename' tooltip='可以是文件夹格式,比如(directory/a.name)' required>
<Input />
</Form.Item>
<Form.Item name={['publish', 'description']} label='Description'>
<Input.TextArea rows={4} />
</Form.Item>
<Form.Item label=' ' colon={false}>
<form className='flex flex-col gap-6 pt-2' onSubmit={handleSubmit(onFinish)}>
<Controller
name='publish.key'
control={control}
defaultValue=''
render={({ field }) => <TextField {...field} label='App key' required fullWidth />}
/>
<Controller
name='publish.version'
control={control}
defaultValue=''
render={({ field }) => <TextField {...field} label='App Version' required fullWidth />}
/>
<Controller
name='publish.fileName'
control={control}
defaultValue=''
render={({ field }) => <TextField {...field} label='Filename' required fullWidth />}
/>
<Controller
name='publish.description'
control={control}
defaultValue=''
render={({ field }) => <TextField {...field} label='Description' multiline rows={4} fullWidth />}
/>
<div className='flex gap-3'>
<Tooltip
placement='top'
title='根据文件名和code的字符串的内容自动生成文件。并保存。如果是其他文件类型转成base64上传。比如图片以类似data:image/jpeg;开头'>
<Button variant='contained' color='primary' type='submit'>
{t('Upload')}
</Button>
</Tooltip>
<Button variant='contained' onClick={onUpdate}>
{t('Submit')}
</Button>
<Button onClick={onClose}></Button>
<Button onClick={onClose}>{t('Cancel')}</Button>
</div>
</Form.Item>
</Form>
</form>
</DialogContent>
</Dialog>
);
};
export const ContainerList = () => {
const navicate = useNewNavigate();
const [modal, contextHolder] = Modal.useModal();
const [modal, contextHolder] = useModal();
const containerStore = useContainerStore(
useShallow((state) => {
return {
@ -271,7 +284,7 @@ export const ContainerList = () => {
e.stopPropagation();
}}>
{item.title || '-'}
<div className='font-thin card-key ml-3'>{item.tags ? item.tags.join(', ') : ''}</div>
<div className='font-thin card-key ml-3'>{item.tags ? item.tags?.join?.(', ') : ''}</div>
</div>
<div className='font-light text-xs mt-2'>{item.description ? item.description : '-'}</div>
</div>

View File

@ -16,4 +16,3 @@ export const App = () => {
);
};
export * from './module/Select';

View File

@ -1,39 +0,0 @@
import { query } from '@/modules';
import { Select as AntSelect, SelectProps } from 'antd';
import { useEffect, useState } from 'react';
import { message } from '@/modules/message';
export const Select = (props: SelectProps) => {
const [options, setOptions] = useState<{ value: string; id: string }[]>([]);
useEffect(() => {
fetch();
}, []);
const fetch = async () => {
const res = await query.post({
path: 'container',
key: 'list',
});
if (res.code !== 200) {
message.error(res.message || '获取容器列表失败');
return;
}
const data = res.data || [];
setOptions(
data.map((item: any) => {
return {
label: item.title,
value: item.id,
};
}),
);
};
return (
<AntSelect
{...props}
options={options}
// onChange={(e) => {
// const labelValue = options.find((item) => item.value === e);
// props.onChange?.(e, options);
// }}
/>
);
};

View File

@ -4,13 +4,17 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import path from 'path-browserify';
import prettyBytes from 'pretty-bytes';
import clsx from 'clsx';
import { isObjectNull } from '@/utils/is-null';
import FileOutlined from '@ant-design/icons/FileOutlined';
import FolderOutlined from '@ant-design/icons/FolderOutlined';
import { IconButton } from '@kevisual/center-components/button/index.tsx';
import { render, unmount } from '@kevisual/resources/pages/Bootstrap.tsx';
import UploadOutlined from '@ant-design/icons/lib/icons/UploadOutlined';
import { Tooltip } from '@mui/material';
import { useResourceFileStore } from '@kevisual/resources/pages/store/resource-file.ts';
import { FileDrawerApp } from '@kevisual/resources/pages/file/draw/FileDrawer.tsx';
import { RefreshCw, Upload } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { UploadButton } from '@kevisual/resources/pages/upload/index.tsx';
export const CardPath = ({ children }: any) => {
const userAppStore = useFileStore(
useShallow((state) => {
@ -32,10 +36,18 @@ export const CardPath = ({ children }: any) => {
userAppStore.setPath(prefix.replace('root/', '') + '/');
userAppStore.getList();
};
const { t } = useTranslation();
const [usrname, appKey, version] = paths;
const onUloadFinish = (res: any) => {
console.log(res);
userAppStore.getList();
};
return (
<div className='border border-gray-200 rounded'>
<div className='p-2'>
<div className='flex flex-col'>
<div className='flex justify-between'>
<div className='flex gap-2 py-2'>
<div>Path: </div>
<div className='flex'>
@ -56,6 +68,32 @@ export const CardPath = ({ children }: any) => {
})}
</div>
</div>
<div className='flex gap-2'>
<Tooltip title={t('refresh')} placement='bottom'>
<IconButton
color='primary'
onClick={() => {
userAppStore.getList();
}}>
<RefreshCw />
</IconButton>
</Tooltip>
{version && (
<>
<Tooltip title={t('uploadDirectory')} placement='bottom'>
<IconButton color='primary'>
<UploadButton onlyIcon uploadDirectory icon={<Upload />} appKey={appKey} version={version} username={usrname} onUpload={onUloadFinish} />
</IconButton>
</Tooltip>
<Tooltip title={t('upload')} placement='bottom'>
<IconButton color='primary'>
<UploadButton onlyIcon appKey={appKey} version={version} username={usrname} onUpload={onUloadFinish} />
</IconButton>
</Tooltip>
</>
)}
</div>
</div>
</div>
</div>
<div className=''>{children}</div>
@ -65,6 +103,17 @@ export const CardPath = ({ children }: any) => {
export const List = () => {
const [tab, setTab] = useState<'folder' | 'upload'>('folder');
const uploadRef = useRef<HTMLDivElement>(null);
const { setOpenDrawer, setPrefix, setResource, getStatFile, setOnce } = useResourceFileStore(
useShallow((state) => {
return {
setOpenDrawer: state.setOpenDrawer,
setPrefix: state.setPrefix,
setResource: state.setResource,
getStatFile: state.getStatFile,
setOnce: state.setOnce,
};
}),
);
const userAppStore = useFileStore(
useShallow((state) => {
return {
@ -79,6 +128,9 @@ export const List = () => {
);
useEffect(() => {
userAppStore.getList();
return () => {
setOnce(null);
};
}, []);
const onDirectoryClick = (prefix: string) => {
userAppStore.setPath(prefix);
@ -114,7 +166,15 @@ export const List = () => {
className=' border-t border-gray-200 flex gap-2 p-2'
key={index}
onClick={() => {
userAppStore.getFile(item.name);
setPrefix(item.name);
setResource(item);
getStatFile();
setOpenDrawer(true);
setOnce((data: any) => {
// console.log(data);
userAppStore.getList();
});
console.log(item);
}}>
<div>
<FileOutlined />
@ -126,9 +186,7 @@ export const List = () => {
})}
</div>
</CardPath>
<div>
<pre>{!isObjectNull(userAppStore.file) && JSON.stringify(userAppStore.file, null, 2)}</pre>
</div>
<FileDrawerApp />
</>
);
}, [userAppStore.list, userAppStore.path]);

View File

@ -1,8 +1,6 @@
import { Input, Modal } from 'antd';
import { Fragment, useEffect, useState } from 'react';
import { useOrgStore } from '../store';
import { useShallow } from 'zustand/react/shallow';
import { Form } from 'antd';
import { useNewNavigate } from '@/modules';
import { useTranslation } from 'react-i18next';
import { Tooltip, Button, ButtonGroup, Dialog, DialogTitle, DialogContent } from '@mui/material';
@ -18,10 +16,14 @@ import clsx from 'clsx';
import { isObjectNull } from '@/utils/is-null';
import { CardBlank } from '@/components/card/CardBlank';
import { useLayoutStore } from '@/modules/layout/store';
import { useModal } from '@kevisual/center-components/modal/Confirm.tsx';
import { useForm, Controller } from 'react-hook-form';
import { TextField } from '@mui/material';
import { pick } from 'lodash-es';
const FormModal = () => {
const [form] = Form.useForm();
const { t } = useTranslation();
const { control, handleSubmit, reset } = useForm();
const userStore = useOrgStore(
useShallow((state) => {
return {
@ -33,47 +35,50 @@ const FormModal = () => {
};
}),
);
useEffect(() => {
const open = userStore.showEdit;
if (open) {
const isNull = isObjectNull(userStore.formData);
if (isNull) {
form.setFieldsValue({});
} else form.setFieldsValue(userStore.formData);
reset({});
} else reset(userStore.formData);
}
}, [userStore.showEdit, userStore.formData]);
const onFinish = async (values: any) => {
userStore.updateData(values);
return () => {
reset({});
};
}, [userStore.showEdit, userStore.formData, reset]);
const onFinish = async (values: any) => {
const pickValues = pick(values, ['id', 'username', 'description']);
userStore.updateData(pickValues);
};
const onClose = () => {
userStore.setShowEdit(false);
form.setFieldsValue({});
reset({});
userStore.setFormData({});
};
const isEdit = userStore.formData.id;
return (
<Dialog open={userStore.showEdit} onClose={() => userStore.setShowEdit(false)}>
<DialogTitle>{isEdit ? 'Edit' : 'Add'}</DialogTitle>
<DialogContent sx={{ padding: '20px', minWidth: '600px' }}>
<Form
form={form}
onFinish={onFinish}
labelCol={{
span: 4,
}}
wrapperCol={{
span: 20,
}}>
<Form.Item name='id' hidden>
<Input />
</Form.Item>
<Form.Item name='username' label='username'>
<Input disabled={isEdit} />
</Form.Item>
<Form.Item name='description' label='description'>
<Input.TextArea rows={4} />
</Form.Item>
<Form.Item label=' ' colon={false}>
<form onSubmit={handleSubmit(onFinish)}>
<Controller
name='username'
control={control}
defaultValue=''
render={({ field }) => <TextField {...field} label='username' disabled={isEdit} fullWidth margin='normal' />}
/>
<Controller
name='description'
control={control}
defaultValue=''
render={({ field }) => <TextField {...field} label='description' multiline rows={4} fullWidth margin='normal' />}
/>
<div className='flex gap-2'>
<Button variant='contained' type='submit'>
{t('Submit')}
@ -82,8 +87,7 @@ const FormModal = () => {
{t('Cancel')}
</Button>
</div>
</Form.Item>
</Form>
</form>
</DialogContent>
</Dialog>
);
@ -111,9 +115,7 @@ export const List = () => {
};
}),
);
const [codeEdit, setCodeEdit] = useState(false);
const [modal, contextHolder] = Modal.useModal();
const [code, setCode] = useState('');
const [modal, contextHolder] = useModal();
useEffect(() => {
userStore.getList();
}, []);
@ -143,9 +145,7 @@ export const List = () => {
className='flex text-sm gap flex-col w-[400px] max-h-[400px] bg-white p-4 rounded-lg'
key={item.id}
onClick={() => {
setCode(item.code);
userStore.setFormData(item);
setCodeEdit(true);
// userStore.setFormData(item);
}}>
<div className='px-4 cursor-pointer'>
<div
@ -168,7 +168,6 @@ export const List = () => {
onClick={(e) => {
userStore.setFormData(item);
userStore.setShowEdit(true);
setCodeEdit(false);
e.stopPropagation();
}}>
<EditOutlined />
@ -214,35 +213,6 @@ export const List = () => {
<CardBlank className='w-[400px]' />
</div>
</div>
<div className={clsx('bg-gray-100 border-gray-200 border-bg-slate-300 w-[600px] shark-0', !codeEdit && 'hidden', 'hidden')}>
<div className='bg-white p-2'>
<div className='mt-2 ml-2 flex gap-2'>
<Button
onClick={() => {
setCodeEdit(false);
userStore.setFormData({});
}}>
<LeftOutlined />
</Button>
<Button
onClick={() => {
userStore.updateData({ ...userStore.formData, code });
}}>
<SaveOutlined />
</Button>
</div>
</div>
<div className='h-[94%] p-2 rounded-2 shadow-xs'>
<Input.TextArea
value={code}
onChange={(value) => {
// setCode(value);
}}
className='h-full max-h-full scrollbar'
style={{ overflow: 'auto' }}
/>
</div>
</div>
</div>
<FormModal />
{contextHolder}

View File

@ -2,22 +2,24 @@ import { useShallow } from 'zustand/react/shallow';
import { useOrgStore } from '../store';
import { useParams } from 'react-router';
import { useEffect } from 'react';
import { Input, Modal, Select } from 'antd';
import { message } from '@/modules/message';
import DeleteOutlined from '@ant-design/icons/DeleteOutlined';
import LeftOutlined from '@ant-design/icons/LeftOutlined';
import PlusOutlined from '@ant-design/icons/PlusOutlined';
import { Tooltip, Button, ButtonGroup, Dialog, DialogTitle, DialogContent } from '@mui/material';
import { IconButton } from '@kevisual/center-components/button/index.tsx';
import { Form } from 'antd';
import { useNewNavigate } from '@/modules';
import { isObjectNull } from '@/utils/is-null';
import copy from 'copy-to-clipboard';
import { useTranslation } from 'react-i18next';
import clsx from 'clsx';
import { useModal } from '@kevisual/center-components/modal/Confirm.tsx';
import { TextField } from '@mui/material';
import { Select } from '@kevisual/center-components/select/index.tsx';
import { useForm, Controller } from 'react-hook-form';
import EditOutlined from '@ant-design/icons/EditOutlined';
const FormModal = () => {
const [form] = Form.useForm();
const { control, handleSubmit, reset } = useForm();
const userStore = useOrgStore(
useShallow((state) => {
return {
@ -29,17 +31,21 @@ const FormModal = () => {
};
}),
);
useEffect(() => {
const open = userStore.showEdit;
if (open) {
const isNull = isObjectNull(userStore.formData);
if (isNull) {
form.setFieldsValue({});
} else form.setFieldsValue(userStore.formData);
reset({});
} else reset(userStore.formData);
}
}, [userStore.showEdit, userStore.formData]);
return () => {
reset({});
};
}, [userStore.showEdit, userStore.formData, reset]);
const onFinish = async (values: any) => {
//
console.log(values);
const username = values.username;
const role = values.role;
@ -47,47 +53,46 @@ const FormModal = () => {
message.error('username is required');
return;
}
userStore.addUser({ username: username, role });
const res = await userStore.addUser({ username: username, role });
if (res?.code === 200) {
userStore.setShowEdit(false);
}
};
const onClose = () => {
userStore.setShowEdit(false);
userStore.setFormData({});
};
const isEdit = userStore.formData.id;
const { t } = useTranslation();
return (
<Dialog open={userStore.showEdit} onClose={() => userStore.setShowEdit(false)}>
<DialogTitle>{isEdit ? 'Edit' : 'Add'}</DialogTitle>
<DialogContent sx={{ padding: '20px', minWidth: '600px' }}>
<Form
form={form}
onFinish={onFinish}
labelCol={{
span: 4,
}}
wrapperCol={{
span: 20,
}}>
<Form.Item name='id' hidden>
<Input />
</Form.Item>
<Form.Item name='username' label='username'>
<Input disabled={isEdit} />
</Form.Item>
<Form.Item name='role' label='role'>
<form className='flex flex-col gap-6' onSubmit={handleSubmit(onFinish)}>
<Controller
name='username'
control={control}
defaultValue=''
render={({ field }) => <TextField {...field} label='username' disabled={isEdit} fullWidth margin='normal' />}
/>
<Controller
name='role'
control={control}
defaultValue=''
render={({ field }) => (
<Select
{...field}
options={[
{
label: 'admin',
value: 'admin',
},
{
label: 'member',
value: 'member',
},
]}></Select>
</Form.Item>
<Form.Item label=' ' colon={false}>
{ label: 'admin', value: 'admin' },
{ label: 'user', value: 'user' },
]}
fullWidth
/>
)}
/>
<div className='flex gap-2'>
<Button variant='contained' type='submit'>
{t('Submit')}
@ -96,8 +101,7 @@ const FormModal = () => {
{t('Cancel')}
</Button>
</div>
</Form.Item>
</Form>
</form>
</DialogContent>
</Dialog>
);
@ -106,7 +110,7 @@ const FormModal = () => {
export const UserList = () => {
const param = useParams();
const navicate = useNewNavigate();
const [modal, contextHolder] = Modal.useModal();
const [modal, contextHolder] = useModal();
const { t } = useTranslation();
const orgStore = useOrgStore(
useShallow((state) => {
@ -173,8 +177,18 @@ export const UserList = () => {
variant='contained'
color='primary'
sx={{ color: 'white', '& .MuiButton-root': { color: 'white', minWidth: '32px', width: '32px', height: '32px', padding: '6px' } }}>
{/* <Tooltip title='edit'>
<Button
disabled={isOwner}
onClick={() => {
orgStore.setUserFormData(item);
orgStore.setShowUserEdit(true);
}}>
<EditOutlined />
</Button>
</Tooltip> */}
<Tooltip title='delete'>
<IconButton
<Button
disabled={isOwner}
onClick={() => {
modal.confirm({
@ -186,7 +200,7 @@ export const UserList = () => {
});
}}>
<DeleteOutlined />
</IconButton>
</Button>
</Tooltip>
</ButtonGroup>
</div>

View File

@ -22,7 +22,7 @@ type OrgStore = {
orgId: string;
setOrgId: (orgId: string) => void;
getOrg: () => Promise<any>;
addUser: (data: { userId?: string; username?: string; role?: string }) => Promise<void>;
addUser: (data: { userId?: string; username?: string; role?: string }) => Promise<any>;
removeUser: (userId: string) => Promise<void>;
};
export const useOrgStore = create<OrgStore>((set, get) => {
@ -113,6 +113,7 @@ export const useOrgStore = create<OrgStore>((set, get) => {
} else {
message.error(res.message || 'Request failed');
}
return res
},
removeUser: async (userId: string) => {
const { orgId } = get();

View File

@ -1,8 +1,6 @@
import { Input, Modal } from 'antd';
import { Fragment, useEffect, useState } from 'react';
import { useUserStore } from '../store';
import { useShallow } from 'zustand/react/shallow';
import { Form } from 'antd';
import EditOutlined from '@ant-design/icons/EditOutlined';
import SaveOutlined from '@ant-design/icons/SaveOutlined';
import DeleteOutlined from '@ant-design/icons/DeleteOutlined';
@ -14,8 +12,13 @@ import { CardBlank } from '@kevisual/center-components/card/CardBlank.tsx';
import { Dialog, ButtonGroup, Button, DialogContent, DialogTitle } from '@mui/material';
import { IconButton } from '@kevisual/center-components/button/index.tsx';
import { useTranslation } from 'react-i18next';
import { useModal } from '@kevisual/center-components/modal/Confirm.tsx';
import { TextField } from '@mui/material';
import { useForm, Controller } from 'react-hook-form';
import { pick } from 'lodash-es';
const FormModal = () => {
const [form] = Form.useForm();
const { control, handleSubmit, reset } = useForm();
const userStore = useUserStore(
useShallow((state) => {
return {
@ -27,25 +30,34 @@ const FormModal = () => {
};
}),
);
useEffect(() => {
const open = userStore.showEdit;
if (open) {
const isNull = isObjectNull(userStore.formData);
if (isNull) {
form.setFieldsValue({});
} else form.setFieldsValue(userStore.formData);
reset({});
} else reset(userStore.formData);
}
}, [userStore.showEdit]);
const onFinish = async (values: any) => {
userStore.updateData(values);
return () => {
reset({});
};
}, [userStore.showEdit]);
const onFinish = (values: any) => {
const pickValues = pick(values, ['id', 'username', 'description']);
userStore.updateData(pickValues);
};
const onClose = () => {
userStore.setShowEdit(false);
form.setFieldsValue({});
reset({});
userStore.setFormData({});
};
const isEdit = userStore.formData.id;
const { t } = useTranslation();
return (
<Dialog
open={userStore.showEdit}
@ -57,39 +69,30 @@ const FormModal = () => {
}}>
<DialogTitle>{isEdit ? t('Edit') : t('Add')}</DialogTitle>
<DialogContent>
<Form
form={form}
onFinish={onFinish}
labelCol={{
span: 4,
}}
wrapperCol={{
span: 20,
}}>
<Form.Item name='id' hidden>
<Input />
</Form.Item>
<Form.Item name='username' label='username'>
<Input />
</Form.Item>
<Form.Item name='description' label='description'>
<Input.TextArea rows={4} />
</Form.Item>
<Form.Item label=' ' colon={false}>
<form onSubmit={handleSubmit(onFinish)}>
<Controller name='username' control={control} defaultValue='' render={({ field }) => <TextField {...field} label='username' fullWidth />} />
<Controller
name='description'
control={control}
defaultValue=''
render={({ field }) => <TextField {...field} label='description' multiline rows={4} fullWidth />}
/>
<div>
<Button type='submit' variant='contained'>
{t('Submit')}
</Button>
<Button className='ml-2' type='reset' onClick={onClose}>
<Button className='ml-2' type='button' onClick={onClose}>
{t('Cancel')}
</Button>
</Form.Item>
</Form>
</div>
</form>
</DialogContent>
</Dialog>
);
};
export const List = () => {
const [modal, contextHolder] = Modal.useModal();
const [modal, contextHolder] = useModal();
const userStore = useUserStore(
useShallow((state) => {
return {
@ -104,8 +107,6 @@ export const List = () => {
};
}),
);
const [codeEdit, setCodeEdit] = useState(false);
const [code, setCode] = useState('');
useEffect(() => {
userStore.getList();
}, []);
@ -132,9 +133,7 @@ export const List = () => {
className='flex text-sm gap flex-col w-[400px] max-h-[400px] bg-white p-4 rounded-lg'
key={item.id}
onClick={() => {
setCode(item.code);
userStore.setFormData(item);
setCodeEdit(true);
// userStore.setFormData(item);
}}>
<div className='px-4 cursor-pointer'>
<div
@ -156,7 +155,6 @@ export const List = () => {
onClick={(e) => {
userStore.setFormData(item);
userStore.setShowEdit(true);
setCodeEdit(false);
e.stopPropagation();
}}>
<EditOutlined />
@ -188,36 +186,6 @@ export const List = () => {
)}
</div>
</div>
<div className={clsx('bg-gray-100 border-l-gray-200 border-bg-slate-300 w-[600px] shark-0', !codeEdit && 'hidden', 'hidden')}>
<div className='bg-white p-2'>
<div className='mt-2 ml-2 flex gap-2'>
<Button
onClick={() => {
setCodeEdit(false);
userStore.setFormData({});
}}>
<LeftOutlined />
</Button>
<Button
onClick={() => {
console.log('save', userStore.formData);
userStore.updateData({ ...userStore.formData, code });
}}>
<SaveOutlined />
</Button>
</div>
</div>
<div className='h-[94%] p-2 rounded-2 shadow-xs'>
<Input.TextArea
value={code}
onChange={(value) => {
// setCode(value);
}}
className='h-full max-h-full scrollbar'
style={{ overflow: 'auto' }}
/>
</div>
</div>
</div>
<FormModal />
{contextHolder}

View File

@ -32,5 +32,6 @@
},
"include": [
"src",
"packages/**/*"
]
}