mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
ui: better config page layout
This commit is contained in:
27 files changed
+735
-364
No files matched your search
@@ -0,0 +1,162 @@
|
|||||||
|
import 'schemastery-react/lib/schemastery-react.css';
|
||||||
|
|
||||||
|
import { Allotment } from 'allotment';
|
||||||
|
import { diffLines } from 'diff';
|
||||||
|
import yaml from 'js-yaml';
|
||||||
|
import React from 'react';
|
||||||
|
import { createSchemasteryReact } from 'schemastery-react';
|
||||||
|
import { ComponentsContext } from './provider';
|
||||||
|
|
||||||
|
const locales = {
|
||||||
|
zh: 'zh-CN',
|
||||||
|
en: 'en-US',
|
||||||
|
};
|
||||||
|
|
||||||
|
function MonacoContainer({
|
||||||
|
config, setValue, setError, monaco, schema, editorCallback, size,
|
||||||
|
}) {
|
||||||
|
const editorRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
const [loading, setLoading] = React.useState(false);
|
||||||
|
const [editor, setEditor] = React.useState<any>(null);
|
||||||
|
const [model, setModel] = React.useState<any>(null);
|
||||||
|
const { codeFontFamily } = React.useContext(ComponentsContext);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!monaco) return;
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
monaco.editor.remeasureFonts();
|
||||||
|
}, 1000);
|
||||||
|
return () => clearInterval(interval); // eslint-disable-line consistent-return
|
||||||
|
}, [monaco]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const listener = () => {
|
||||||
|
if (!editor) return;
|
||||||
|
editor.layout();
|
||||||
|
};
|
||||||
|
window.addEventListener('resize', listener);
|
||||||
|
return () => window.removeEventListener('resize', listener);
|
||||||
|
}, [editor]);
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!editor) return;
|
||||||
|
editor.layout();
|
||||||
|
}, [size]);
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!editor) return;
|
||||||
|
const disposable = editor.onDidChangeModelContent(() => {
|
||||||
|
const val = editor.getValue({ lineEnding: '\n', preserveBOM: false });
|
||||||
|
try {
|
||||||
|
const loaded = yaml.load(val);
|
||||||
|
schema(loaded);
|
||||||
|
setValue(val);
|
||||||
|
setError('');
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return () => disposable.dispose(); // eslint-disable-line
|
||||||
|
}, [editor, setValue, setError]);
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!editorRef.current || loading) return;
|
||||||
|
setLoading(true);
|
||||||
|
// eslint-disable-next-line
|
||||||
|
const model = monaco.editor.createModel(config, 'yaml', monaco.Uri.parse('hydro://system/setting.yaml'));
|
||||||
|
setModel(model);
|
||||||
|
const e = monaco.editor.create(editorRef.current, {
|
||||||
|
theme: 'vs-light',
|
||||||
|
lineNumbers: 'off',
|
||||||
|
glyphMargin: true,
|
||||||
|
lightbulb: { enabled: monaco.editor.ShowLightbulbIconMode.On },
|
||||||
|
model,
|
||||||
|
minimap: { enabled: false },
|
||||||
|
hideCursorInOverviewRuler: true,
|
||||||
|
overviewRulerLanes: 0,
|
||||||
|
overviewRulerBorder: false,
|
||||||
|
fontFamily: codeFontFamily,
|
||||||
|
fontLigatures: '',
|
||||||
|
unicodeHighlight: {
|
||||||
|
ambiguousCharacters: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
editorCallback(e, model, editorRef.current);
|
||||||
|
setEditor(e);
|
||||||
|
}, [editorRef.current]);
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!editor) return;
|
||||||
|
const current = editor.getValue({ lineEnding: '\n', preserveBOM: false });
|
||||||
|
const diff = diffLines(current, config);
|
||||||
|
const ops: { range: any; text: string }[] = [];
|
||||||
|
let cursor = 1;
|
||||||
|
for (const line of diff) {
|
||||||
|
if (line.added) {
|
||||||
|
let range = model.getFullModelRange();
|
||||||
|
range = range.setStartPosition(cursor, 0);
|
||||||
|
range = range.setEndPosition(cursor, 0);
|
||||||
|
ops.push({ range, text: line.value });
|
||||||
|
} else if (line.removed) {
|
||||||
|
let range = model.getFullModelRange();
|
||||||
|
range = range.setStartPosition(cursor, 0);
|
||||||
|
cursor += line.count;
|
||||||
|
range = range.setEndPosition(cursor, 0);
|
||||||
|
ops.push({ range, text: '' });
|
||||||
|
} else cursor += line.count;
|
||||||
|
}
|
||||||
|
model.pushEditOperations([], ops, undefined);
|
||||||
|
}, [editor, config]);
|
||||||
|
return (
|
||||||
|
<div ref={editorRef} style={{
|
||||||
|
width: '100%', height: '500px',
|
||||||
|
}} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ConfigEditor({
|
||||||
|
schema, config, monaco, Markdown, onSave, registerAction, sidebar,
|
||||||
|
}) {
|
||||||
|
const [value, setValue] = React.useState(yaml.load(config));
|
||||||
|
const [info, setInfo] = React.useState('');
|
||||||
|
const [stringConfig, setStringConfig] = React.useState(config);
|
||||||
|
const initial = React.useMemo(() => yaml.load(config), []);
|
||||||
|
const { i18n } = React.useContext(ComponentsContext);
|
||||||
|
|
||||||
|
const Form = React.useMemo(() => createSchemasteryReact({
|
||||||
|
locale: locales[i18n('__id')] || 'en-US',
|
||||||
|
Markdown,
|
||||||
|
}), []);
|
||||||
|
|
||||||
|
const updateFromForm = React.useCallback((v) => {
|
||||||
|
const newDump = yaml.dump(v);
|
||||||
|
if (newDump === stringConfig) return;
|
||||||
|
setStringConfig(newDump);
|
||||||
|
setValue(v);
|
||||||
|
}, [stringConfig]);
|
||||||
|
const updateFromMonaco = React.useCallback((v) => {
|
||||||
|
if (v === stringConfig) return;
|
||||||
|
setStringConfig(v);
|
||||||
|
setValue(yaml.load(v));
|
||||||
|
}, [stringConfig]);
|
||||||
|
|
||||||
|
const [size, setSize] = React.useState([50, 50]);
|
||||||
|
|
||||||
|
return (<div className="fullscreen-content" style={{ backgroundColor: 'white', zIndex: 10 }}>
|
||||||
|
<Allotment onChange={setSize}>
|
||||||
|
<Allotment.Pane>
|
||||||
|
<MonacoContainer
|
||||||
|
editorCallback={registerAction} schema={schema} monaco={monaco}
|
||||||
|
config={stringConfig} setValue={updateFromMonaco} setError={setInfo}
|
||||||
|
size={size[0]}
|
||||||
|
/>
|
||||||
|
<pre className="help-text">{info}</pre>
|
||||||
|
<button onClick={() => onSave(stringConfig)} className="rounded primary button">{i18n('Save All Changes')}</button>
|
||||||
|
</Allotment.Pane>
|
||||||
|
<Allotment.Pane>
|
||||||
|
<div style={{
|
||||||
|
overflowY: 'scroll', top: 0, bottom: 0, left: 0, right: 0, position: 'absolute', marginLeft: 10,
|
||||||
|
}}>
|
||||||
|
<Form schema={schema} initial={initial} value={value} onChange={updateFromForm} />
|
||||||
|
</div>
|
||||||
|
</Allotment.Pane>
|
||||||
|
{sidebar && <Allotment.Pane maxSize={220}>{sidebar}</Allotment.Pane>}
|
||||||
|
</Allotment>
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default function IconComponent(props: { name: string, className?: string } & React.HTMLAttributes<HTMLSpanElement>) {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
className,
|
||||||
|
...rest
|
||||||
|
} = props;
|
||||||
|
const cn = classNames(className, `icon icon-${name}`);
|
||||||
|
return (
|
||||||
|
<span {...rest} className={cn} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
IconComponent.propTypes = {
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
className: PropTypes.string,
|
||||||
|
};
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export function Row({ children }: { children: React.ReactNode }) {
|
||||||
|
return <div className="row">
|
||||||
|
{children}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Column({ children, md }: { children: React.ReactNode, md?: number }) {
|
||||||
|
return <div className={`columns medium-${md || 12}`}>
|
||||||
|
{children}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
export default class Notification {
|
||||||
|
type: string;
|
||||||
|
action: any;
|
||||||
|
$dom: JQuery<HTMLElement>;
|
||||||
|
$n: JQuery<HTMLElement>;
|
||||||
|
duration: number;
|
||||||
|
autoHideTimer?: NodeJS.Timeout;
|
||||||
|
|
||||||
|
static async success(message: string, duration?: number) {
|
||||||
|
return toast.success(message, { autoClose: duration });
|
||||||
|
}
|
||||||
|
|
||||||
|
static async info(message: string, duration?: number) {
|
||||||
|
return toast.info(message, { autoClose: duration });
|
||||||
|
}
|
||||||
|
|
||||||
|
static async warn(message: string, duration?: number) {
|
||||||
|
return toast.warning(message, { autoClose: duration });
|
||||||
|
}
|
||||||
|
|
||||||
|
static async error(message: string, duration?: number) {
|
||||||
|
return toast.error(message, { autoClose: duration });
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-1
@@ -1,3 +1,5 @@
|
|||||||
|
import './autocomplete.scss';
|
||||||
|
|
||||||
import { debounce, uniqueId } from 'lodash';
|
import { debounce, uniqueId } from 'lodash';
|
||||||
import React, {
|
import React, {
|
||||||
forwardRef, useEffect,
|
forwardRef, useEffect,
|
||||||
@@ -5,7 +7,7 @@ import React, {
|
|||||||
} from 'react';
|
} from 'react';
|
||||||
import { DndProvider, useDrag, useDrop } from 'react-dnd';
|
import { DndProvider, useDrag, useDrop } from 'react-dnd';
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
import Icon from 'vj/components/react/IconComponent';
|
import Icon from '../Icon';
|
||||||
|
|
||||||
export interface AutoCompleteProps<Item> {
|
export interface AutoCompleteProps<Item> {
|
||||||
width?: string;
|
width?: string;
|
||||||
+17
-17
@@ -1,3 +1,4 @@
|
|||||||
|
import { defaults } from 'lodash';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import AutoComplete from './AutoComplete';
|
import AutoComplete from './AutoComplete';
|
||||||
@@ -7,18 +8,27 @@ type DefaultProps = {
|
|||||||
name: string;
|
name: string;
|
||||||
} & string;
|
} & string;
|
||||||
|
|
||||||
const CustomSelectAutoComplete = React.forwardRef<any, any>((props, ref) => (
|
const CustomSelectAutoComplete = React.forwardRef<any, any>((props, ref) => {
|
||||||
<AutoComplete<DefaultProps>
|
const def = defaults(props, {
|
||||||
|
width: '100%',
|
||||||
|
height: 'auto',
|
||||||
|
listStyle: {},
|
||||||
|
multi: false,
|
||||||
|
selectedKeys: [],
|
||||||
|
freeSolo: false,
|
||||||
|
freeSoloConverter: (input) => input,
|
||||||
|
});
|
||||||
|
return <AutoComplete<DefaultProps>
|
||||||
ref={ref as any}
|
ref={ref as any}
|
||||||
fetchItems={(keys) => props.data.filter((i) => (i._id ? keys.includes(i._id) : keys.includes(i)))}
|
fetchItems={(keys) => def.data.filter((i) => (i._id ? keys.includes(i._id) : keys.includes(i)))}
|
||||||
queryItems={(query) => props.data.filter((i) => (i.name || i).toString().toLowerCase().includes(query.toLowerCase()))}
|
queryItems={(query) => def.data.filter((i) => (i.name || i).toString().toLowerCase().includes(query.toLowerCase()))}
|
||||||
itemText={(item) => `${item.name || item}`}
|
itemText={(item) => `${item.name || item}`}
|
||||||
itemKey={(item) => `${item._id?.toString() || item.name || item}`}
|
itemKey={(item) => `${item._id?.toString() || item.name || item}`}
|
||||||
renderItem={(item) => `${item.name || item}`}
|
renderItem={(item) => `${item.name || item}`}
|
||||||
allowEmptyQuery
|
allowEmptyQuery
|
||||||
{...props}
|
{...def}
|
||||||
/>
|
/>;
|
||||||
));
|
});
|
||||||
|
|
||||||
CustomSelectAutoComplete.propTypes = {
|
CustomSelectAutoComplete.propTypes = {
|
||||||
width: PropTypes.string,
|
width: PropTypes.string,
|
||||||
@@ -35,14 +45,4 @@ CustomSelectAutoComplete.propTypes = {
|
|||||||
|
|
||||||
CustomSelectAutoComplete.displayName = 'CustomSelectAutoComplete';
|
CustomSelectAutoComplete.displayName = 'CustomSelectAutoComplete';
|
||||||
|
|
||||||
CustomSelectAutoComplete.defaultProps = {
|
|
||||||
width: '100%',
|
|
||||||
height: 'auto',
|
|
||||||
listStyle: {},
|
|
||||||
multi: false,
|
|
||||||
selectedKeys: [],
|
|
||||||
freeSolo: false,
|
|
||||||
freeSoloConverter: (input) => input,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CustomSelectAutoComplete;
|
export default CustomSelectAutoComplete;
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
@use '../common' as *;
|
||||||
|
|
||||||
|
.autocomplete-dummy {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-wrapper {
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
padding: 1px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
@include form-styles();
|
||||||
|
background-color: $input-background-color;
|
||||||
|
outline: $input-outline;
|
||||||
|
transition: outline-color .2s, border-color .2s;
|
||||||
|
transition-timing-function: ease-out-cubic;
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&.focused {
|
||||||
|
border-color: $input-focus-border-color;
|
||||||
|
outline: $input-focus-outline;
|
||||||
|
outline-offset: 0;
|
||||||
|
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overwrite, for IE compatibility
|
||||||
|
&:read-only {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #40a9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
background-color: $input-background-color;
|
||||||
|
color: rgba(0, 0, 0, .85);
|
||||||
|
height: 30px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 4px 6px;
|
||||||
|
width: 0;
|
||||||
|
min-width: 30px;
|
||||||
|
flex-grow: 1;
|
||||||
|
border: 0;
|
||||||
|
margin: 0;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-tag {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 24px;
|
||||||
|
margin: 2px;
|
||||||
|
line-height: 22px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
border-radius: 2px;
|
||||||
|
box-sizing: content-box;
|
||||||
|
padding: 0 4px 0 10px;
|
||||||
|
outline: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: #40a9ff;
|
||||||
|
background-color: #e6f7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&>div {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
&>span.bp5-icon>svg {
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-list {
|
||||||
|
min-width: 300px;
|
||||||
|
margin: 2px 0 0;
|
||||||
|
padding: 0;
|
||||||
|
position: absolute;
|
||||||
|
list-style: none;
|
||||||
|
background-color: #fff;
|
||||||
|
overflow: auto;
|
||||||
|
max-height: 500px;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
z-index: 999;
|
||||||
|
|
||||||
|
li {
|
||||||
|
padding: 5px 12px;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
&>div {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&>span.bp5-icon>svg {
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[aria-selected='true'] {
|
||||||
|
background-color: #fafafa;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&>span.bp5-icon>svg {
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-focus='true'] {
|
||||||
|
background-color: #e6f7ff;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&>span.bp5-icon>svg {
|
||||||
|
color: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
@use "sass:color";
|
||||||
|
@use "sass:list";
|
||||||
|
@use "sass:math";
|
||||||
|
|
||||||
|
@function toRem($value) {
|
||||||
|
$remValue: math.div($value, 16) + rem;
|
||||||
|
@return $remValue;
|
||||||
|
}
|
||||||
|
@function rem($values) {
|
||||||
|
$remValues: ();
|
||||||
|
@each $value in $values {
|
||||||
|
$remValues: list.append($remValues, toRem($value));
|
||||||
|
}
|
||||||
|
@return $remValues;
|
||||||
|
}
|
||||||
|
@function lighten($color, $amount) {
|
||||||
|
@return color.adjust($color, $lightness: $amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// layout
|
||||||
|
$grid-padding: 15px; // half gutter
|
||||||
|
|
||||||
|
// fonts
|
||||||
|
$primary-font-family: var(--font-family);
|
||||||
|
$header-font-family: var(--font-family);
|
||||||
|
$code-font-family: var(--code-font-family);
|
||||||
|
|
||||||
|
// primary palette
|
||||||
|
$default-color: #ddd;
|
||||||
|
$primary-color: #5f9fd6;
|
||||||
|
$secondary-color: #ed5f82;
|
||||||
|
$highlight-color: #df6589;
|
||||||
|
$code-color: #c92a2a;
|
||||||
|
$text-1-color: #555;
|
||||||
|
$text-2-color: lighten($text-1-color, 15%);
|
||||||
|
$text-3-color: lighten($text-1-color, 30%);
|
||||||
|
$header-1-color: #3d3d3d;
|
||||||
|
$header-2-color: lighten($header-1-color, 20%);
|
||||||
|
$blockquote-color: #999;
|
||||||
|
$page-bg-color: #edf0f2;
|
||||||
|
$content-bg-color: #fff;
|
||||||
|
$border-1-color: $default-color;
|
||||||
|
$border-2-color: lighten($default-color, 20%);
|
||||||
|
|
||||||
|
$immersive-primary-color: #ffef87;
|
||||||
|
$immersive-text-color: #ffffff;
|
||||||
|
$immersive-header-color: #ffffff;
|
||||||
|
|
||||||
|
$success-color: #25ad40;
|
||||||
|
$fail-color: #fb5555;
|
||||||
|
$progress-color: #f39800;
|
||||||
|
$invalid-color: #9fa0a0;
|
||||||
|
|
||||||
|
// sizes
|
||||||
|
$font-size: 16px; // main content
|
||||||
|
$font-size-secondary: 14px; // secondary content
|
||||||
|
$font-size-small: 13px; // additional informations, usually gray colored
|
||||||
|
$font-size-icon: 16px;
|
||||||
|
$font-size-title: 20px;
|
||||||
|
|
||||||
|
// components
|
||||||
|
$section-gap-v: 15px;
|
||||||
|
$section-gap-h: $grid-padding;
|
||||||
|
$section-list-v: 25px;
|
||||||
|
$section-margin: 30px;
|
||||||
|
$section-shadow: 0 rem(6px) rem(22px) rgba(#AFC2C9, 0.5);
|
||||||
|
$section-bg-color: $content-bg-color;
|
||||||
|
$section-title-color: $header-2-color;
|
||||||
|
$section-toolbtn-color: #AAA;
|
||||||
|
$section-toolbtn-color-hover: #444;
|
||||||
|
|
||||||
|
$autocomplete-empty-row-color: #AAA;
|
||||||
|
$userselect-uid-color: #BBB;
|
||||||
|
|
||||||
|
$editor-border-color: $border-1-color;
|
||||||
|
|
||||||
|
$datepicker-shadow: 0 rem(2px) rem(7px) rgba(#000, 0.3);
|
||||||
|
$datapicker-list-item-bg-color-hover: #F0F0F0;
|
||||||
|
|
||||||
|
$dialog-layer-bg-color: rgba(#F0F0F0, 0.6);
|
||||||
|
$dialog-bg-color: $content-bg-color;
|
||||||
|
$dialog-border-color: $border-1-color;
|
||||||
|
$dialog-shadow: 0 rem(2px) rem(10px) rgba(#000, 0.2);
|
||||||
|
$dialog-title-color: $header-2-color;
|
||||||
|
|
||||||
|
$comment-placeholder-color: #AAA;
|
||||||
|
$comment-op-color: lighten($text-1-color, 30%);
|
||||||
|
$comment-op-link-color: lighten($text-1-color, 50%);
|
||||||
|
|
||||||
|
$menu-item-bg-color-hover: #F4F4F4;
|
||||||
|
$menu-drop-shadow: 0 rem(2px) rem(7px) rgba(#000, 0.3);
|
||||||
|
$menu-drop-bg-color: $content-bg-color;
|
||||||
|
$menu-drop-triangle-size: 4px;
|
||||||
|
$menu-drop-triangle-shadow: rem(-2px) rem(-2px) rem(2px) rgba(#000, 0.15);
|
||||||
|
|
||||||
|
$nav-item-height: 45px;
|
||||||
|
$nav-item-hover-color: $primary-color;
|
||||||
|
$nav-item-active-color: $secondary-color;
|
||||||
|
$nav-item-active-immersive-color: #9AE3F3;
|
||||||
|
$nav-item-round-height: 32px;
|
||||||
|
$nav-item-round-color: $primary-color;
|
||||||
|
$nav-item-round-border: 2px;
|
||||||
|
// $nav-logo-height: 32px;
|
||||||
|
// $nav-logo-height-mobile: 23px;
|
||||||
|
|
||||||
|
$header-layer-height: 100px;
|
||||||
|
$header-bg-width: 1920px;
|
||||||
|
$header-bg-height: 320px;
|
||||||
|
$header-bg-height-mini: $header-bg-height - $header-layer-height;
|
||||||
|
//$header-logo-width = 280px
|
||||||
|
//$header-logo-height = 150px
|
||||||
|
//$header-logo-width-real = 280px
|
||||||
|
//$header-logo-height-real = 150px
|
||||||
|
$header-logo-width: 252px;
|
||||||
|
$header-logo-height: 150px;
|
||||||
|
$header-logo-width-real: 381px;
|
||||||
|
$header-logo-height-real: 201px;
|
||||||
|
$header-logo-domain-lh: 60px;
|
||||||
|
$header-logo-system-lh: 30px;
|
||||||
|
|
||||||
|
$footer-bg-color: $content-bg-color;
|
||||||
|
|
||||||
|
$input-focus-color: #C1E0FF;
|
||||||
|
$form-control-height: 35px;
|
||||||
|
$compact-control-height: 28px;
|
||||||
|
$input-margin: 0 0 1rem;
|
||||||
|
$input-multi-line-margin: 0 0.3rem 0.15rem 0;
|
||||||
|
$input-image-radio-margin: 0 0.3rem 0.3rem 0;
|
||||||
|
$input-background-color: $content-bg-color;
|
||||||
|
$input-border: 1px solid $border-1-color;
|
||||||
|
$input-outline: 2px solid transparent;
|
||||||
|
$input-focus-border-color: $primary-color;
|
||||||
|
$input-focus-outline: 2px solid $input-focus-color;
|
||||||
|
$label-color: lighten($text-1-color, 10%);
|
||||||
|
$help-text-color: lighten($text-1-color, 30%);
|
||||||
|
|
||||||
|
$table-border-color: $border-2-color;
|
||||||
|
$table-alternate-bg-color: #F4F4F4;
|
||||||
|
$table-header-bg-color: rgba(#FFF, 0.95);
|
||||||
|
$table-header-shadow: rem(0 3px 2px) rgba(#000, 0.03);
|
||||||
|
$table-header-shadow-floating: rem(0 2px 5px) rgba(#000, 0.2);
|
||||||
|
$table-padding-h: 10px;
|
||||||
|
$table-lh: 26px;
|
||||||
|
|
||||||
|
$toolbar-bg: #f2f6f7;
|
||||||
|
$toolbar-fore: #8395a1;
|
||||||
|
$toolbar-fore-hover: #5a6b75;
|
||||||
|
$toolbar-bg-hover: #e3e7e8;
|
||||||
|
$toolbar-fore-selected: #FFF;
|
||||||
|
$toolbar-bg-selected: $secondary-color;
|
||||||
|
$toolbar-disabled-opacity: 0.3;
|
||||||
|
|
||||||
|
$supplementary-color: lighten($text-1-color, 50%);
|
||||||
|
$supplementary-border-color: lighten($supplementary-color, 62%);
|
||||||
|
$supplementary-link-color: lighten($text-1-color, 40%);
|
||||||
|
|
||||||
|
$pager-border-color: $table-border-color;
|
||||||
|
|
||||||
|
$nprogress-color: $primary-color;
|
||||||
|
|
||||||
|
$syntax-hl-bg-color: #F8F8F8;
|
||||||
|
|
||||||
|
// // Notification
|
||||||
|
// $notification-color = {
|
||||||
|
// 'success': #9BDB7B,
|
||||||
|
// 'info': #78D6F4,
|
||||||
|
// 'warn': #FCD279,
|
||||||
|
// 'error': #FD848D,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// $notification-icon = {
|
||||||
|
// 'success': $icon-check--circle,
|
||||||
|
// 'info': $icon-info--circle,
|
||||||
|
// 'warn': $icon-info--circle,
|
||||||
|
// 'error': $icon-close--circle,
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
@mixin form-styles() {
|
||||||
|
appearance: none;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
font-size: rem($font-size-secondary);
|
||||||
|
margin: $input-margin;
|
||||||
|
height: rem($form-control-height);
|
||||||
|
line-height: 1.2;
|
||||||
|
padding: rem(5px);
|
||||||
|
border: $input-border;
|
||||||
|
|
||||||
|
&.inline {
|
||||||
|
display: inline-block;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-content {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: $nav-item-height;
|
||||||
|
width: 100%;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export { default as ComponentsProvider } from './provider';
|
||||||
|
export { default as ConfigEditor } from './ConfigEditor';
|
||||||
|
export { default as Icon } from './Icon';
|
||||||
|
export { default as Notification } from './Notification';
|
||||||
|
export { default as AutoComplete, type AutoCompleteHandle, type AutoCompleteProps } from './autocomplete/AutoComplete';
|
||||||
|
export { default as CustomSelectAutoComplete } from './autocomplete/CustomSelectAutoComplete';
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { ToastContainer } from 'react-toastify';
|
||||||
|
|
||||||
|
interface C {
|
||||||
|
i18n: (key: string, ...args: any[]) => string;
|
||||||
|
theme: 'light' | 'dark';
|
||||||
|
codeFontFamily: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
export const ComponentsContext = React.createContext<C>({
|
||||||
|
i18n: (key) => key,
|
||||||
|
theme: 'light',
|
||||||
|
codeFontFamily: 'monospace',
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function ComponentsProvider(props: { children: React.ReactNode } & C) {
|
||||||
|
return <ComponentsContext.Provider value={props}>
|
||||||
|
<ToastContainer
|
||||||
|
position="bottom-left"
|
||||||
|
autoClose={5000}
|
||||||
|
hideProgressBar={false}
|
||||||
|
newestOnTop={false}
|
||||||
|
closeOnClick={false}
|
||||||
|
rtl={false}
|
||||||
|
pauseOnFocusLoss
|
||||||
|
draggable
|
||||||
|
pauseOnHover
|
||||||
|
theme={props.theme}
|
||||||
|
/>
|
||||||
|
{props.children}
|
||||||
|
</ComponentsContext.Provider>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "@hydrooj/components",
|
||||||
|
"version": "1.0.0-alpha.0",
|
||||||
|
"main": "./frontend/index.ts",
|
||||||
|
"dependencies": {
|
||||||
|
"diff": "^7.0.0",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"react-dnd": "^16.0.1",
|
||||||
|
"react-dnd-html5-backend": "^16.0.1",
|
||||||
|
"react-toastify": "^11.0.5",
|
||||||
|
"schemastery-react": "^0.1.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/diff": "^7.0.2",
|
||||||
|
"@types/js-yaml": "^4.0.9",
|
||||||
|
"@types/lodash": "^4.17.16",
|
||||||
|
"sass": "^1.86.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -88,6 +88,14 @@ export default async function (env: { watch?: boolean, production?: boolean, mea
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scssLoader() {
|
||||||
|
return {
|
||||||
|
loader: 'sass-loader',
|
||||||
|
options: {
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const config: import('webpack').Configuration = {
|
const config: import('webpack').Configuration = {
|
||||||
// bail: !env.production,
|
// bail: !env.production,
|
||||||
mode: (env.production || env.measure) ? 'production' : 'development',
|
mode: (env.production || env.measure) ? 'production' : 'development',
|
||||||
@@ -200,6 +208,10 @@ export default async function (env: { watch?: boolean, production?: boolean, mea
|
|||||||
test: /\.styl$/,
|
test: /\.styl$/,
|
||||||
use: [extractCssLoader(), cssLoader(), postcssLoader(), stylusLoader()],
|
use: [extractCssLoader(), cssLoader(), postcssLoader(), stylusLoader()],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
test: /\.scss$/,
|
||||||
|
use: [extractCssLoader(), cssLoader(), postcssLoader(), scssLoader()],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
test: /\.css$/,
|
test: /\.css$/,
|
||||||
use: [extractCssLoader(), cssLoader(), postcssLoader()],
|
use: [extractCssLoader(), cssLoader(), postcssLoader()],
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
import { CustomSelectAutoComplete as CustomSelectAutoCompleteFC } from '@hydrooj/components';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import AutoComplete, { AutoCompleteOptions } from '.';
|
import AutoComplete, { AutoCompleteOptions } from '.';
|
||||||
import CustomSelectAutoCompleteFC from './components/CustomSelectAutoComplete';
|
|
||||||
|
|
||||||
interface CustomSelectOptions {
|
interface CustomSelectOptions {
|
||||||
data: any[]
|
data: any[]
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
.autocomplete-dummy
|
|
||||||
display: none !important
|
|
||||||
|
|
||||||
.autocomplete-wrapper
|
|
||||||
border: 1px solid #d9d9d9
|
|
||||||
padding: 1px
|
|
||||||
display: flex
|
|
||||||
flex-wrap: wrap
|
|
||||||
overflow-y: auto
|
|
||||||
|
|
||||||
form-styles()
|
|
||||||
background-color: $input-background-color
|
|
||||||
outline: $input-outline
|
|
||||||
transition: outline-color .2s, border-color .2s
|
|
||||||
transition-timing-function: ease-out-cubic
|
|
||||||
|
|
||||||
&:focus, &.focused
|
|
||||||
border-color: $input-focus-border-color
|
|
||||||
outline: $input-focus-outline
|
|
||||||
outline-offset: 0
|
|
||||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2)
|
|
||||||
|
|
||||||
// Overwrite, for IE compatibility
|
|
||||||
&:read-only
|
|
||||||
outline: 0
|
|
||||||
|
|
||||||
&:disabled
|
|
||||||
opacity: 0.5
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
border-color: #40a9ff
|
|
||||||
|
|
||||||
input
|
|
||||||
background-color: $input-background-color
|
|
||||||
color: rgba(0,0,0,.85)
|
|
||||||
height: 30px
|
|
||||||
box-sizing: border-box
|
|
||||||
padding: 4px 6px
|
|
||||||
width: 0
|
|
||||||
min-width: 30px
|
|
||||||
flex-grow: 1
|
|
||||||
border: 0
|
|
||||||
margin: 0
|
|
||||||
outline: 0
|
|
||||||
|
|
||||||
.autocomplete-tag
|
|
||||||
display: flex
|
|
||||||
align-items: center
|
|
||||||
height: 24px
|
|
||||||
margin: 2px
|
|
||||||
line-height: 22px
|
|
||||||
background-color: #fafafa
|
|
||||||
border: 1px solid #e8e8e8
|
|
||||||
border-radius: 2px
|
|
||||||
box-sizing: content-box
|
|
||||||
padding: 0 4px 0 10px
|
|
||||||
outline: 0
|
|
||||||
overflow: hidden
|
|
||||||
|
|
||||||
&:focus
|
|
||||||
border-color: #40a9ff
|
|
||||||
background-color: #e6f7ff
|
|
||||||
|
|
||||||
&>div
|
|
||||||
overflow: hidden
|
|
||||||
white-space: nowrap
|
|
||||||
text-overflow: ellipsis
|
|
||||||
|
|
||||||
&>span.bp5-icon>svg
|
|
||||||
font-size: 12px
|
|
||||||
cursor: pointer
|
|
||||||
padding: 4px
|
|
||||||
|
|
||||||
.autocomplete-list
|
|
||||||
min-width: 300px
|
|
||||||
margin: 2px 0 0
|
|
||||||
padding: 0
|
|
||||||
position: absolute
|
|
||||||
list-style: none
|
|
||||||
background-color: #fff
|
|
||||||
overflow: auto
|
|
||||||
max-height: 500px
|
|
||||||
border-radius: 4px
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15)
|
|
||||||
z-index: 999
|
|
||||||
|
|
||||||
li
|
|
||||||
padding: 5px 12px
|
|
||||||
display: flex
|
|
||||||
|
|
||||||
&>div
|
|
||||||
flex-grow: 1
|
|
||||||
|
|
||||||
&>span.bp5-icon>svg
|
|
||||||
color: transparent
|
|
||||||
|
|
||||||
&[aria-selected='true']
|
|
||||||
background-color: #fafafa
|
|
||||||
font-weight: 600
|
|
||||||
|
|
||||||
&>span.bp5-icon>svg
|
|
||||||
color: #1890ff
|
|
||||||
|
|
||||||
&[data-focus='true']
|
|
||||||
background-color: #e6f7ff
|
|
||||||
cursor: pointer
|
|
||||||
|
|
||||||
&>span.bp5-icon>svg
|
|
||||||
color: currentColor
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import { AutoComplete, AutoCompleteHandle, AutoCompleteProps } from '@hydrooj/components';
|
||||||
import type { DomainDoc } from 'hydrooj/src/interface';
|
import type { DomainDoc } from 'hydrooj/src/interface';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { forwardRef } from 'react';
|
import React, { forwardRef } from 'react';
|
||||||
import { request } from 'vj/utils';
|
import { request } from 'vj/utils';
|
||||||
import AutoComplete, { AutoCompleteHandle, AutoCompleteProps } from './AutoComplete';
|
|
||||||
|
|
||||||
const DomainSelectAutoComplete = forwardRef<AutoCompleteHandle<DomainDoc>, AutoCompleteProps<DomainDoc>>((props, ref) => (
|
const DomainSelectAutoComplete = forwardRef<AutoCompleteHandle<DomainDoc>, AutoCompleteProps<DomainDoc>>((props, ref) => (
|
||||||
<AutoComplete<DomainDoc>
|
<AutoComplete<DomainDoc>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import { AutoComplete } from '@hydrooj/components';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import AutoComplete from './AutoComplete';
|
|
||||||
|
|
||||||
type DefaultProps = {
|
type DefaultProps = {
|
||||||
_id: string;
|
_id: string;
|
||||||
|
|||||||
+1
-1
@@ -1,8 +1,8 @@
|
|||||||
|
import { AutoComplete, AutoCompleteHandle, AutoCompleteProps } from '@hydrooj/components';
|
||||||
import type { ProblemDoc } from 'hydrooj/src/interface';
|
import type { ProblemDoc } from 'hydrooj/src/interface';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { forwardRef } from 'react';
|
import React, { forwardRef } from 'react';
|
||||||
import { api, gql, request } from 'vj/utils';
|
import { api, gql, request } from 'vj/utils';
|
||||||
import AutoComplete, { AutoCompleteHandle, AutoCompleteProps } from './AutoComplete';
|
|
||||||
|
|
||||||
const ProblemSelectAutoComplete = forwardRef<AutoCompleteHandle<ProblemDoc>, AutoCompleteProps<ProblemDoc>>((props, ref) => (
|
const ProblemSelectAutoComplete = forwardRef<AutoCompleteHandle<ProblemDoc>, AutoCompleteProps<ProblemDoc>>((props, ref) => (
|
||||||
<AutoComplete<ProblemDoc>
|
<AutoComplete<ProblemDoc>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import { AutoComplete, AutoCompleteHandle, AutoCompleteProps } from '@hydrooj/components';
|
||||||
import type { Udoc } from 'hydrooj/src/interface';
|
import type { Udoc } from 'hydrooj/src/interface';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { forwardRef } from 'react';
|
import { forwardRef } from 'react';
|
||||||
import { api, gql } from 'vj/utils';
|
import { api, gql } from 'vj/utils';
|
||||||
import AutoComplete, { AutoCompleteHandle, AutoCompleteProps } from './AutoComplete';
|
|
||||||
|
|
||||||
const UserSelectAutoComplete = forwardRef<AutoCompleteHandle<Udoc>, AutoCompleteProps<Udoc>>((props, ref) => (
|
const UserSelectAutoComplete = forwardRef<AutoCompleteHandle<Udoc>, AutoCompleteProps<Udoc>>((props, ref) => (
|
||||||
<AutoComplete<Udoc>
|
<AutoComplete<Udoc>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React from 'react';
|
import { AutoComplete as AutoCompleteFC } from '@hydrooj/components';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
||||||
import AutoCompleteFC from './components/AutoComplete';
|
|
||||||
|
|
||||||
export interface AutoCompleteOptions<Multi extends boolean = boolean> {
|
export interface AutoCompleteOptions<Multi extends boolean = boolean> {
|
||||||
multi?: Multi;
|
multi?: Multi;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
import { CustomSelectAutoComplete } from '@hydrooj/components';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { i18n } from 'vj/utils';
|
import { i18n } from 'vj/utils';
|
||||||
import CustomSelectAutoComplete from '../autocomplete/components/CustomSelectAutoComplete';
|
|
||||||
import FileSelectAutoComplete from '../autocomplete/components/FileSelectAutoComplete';
|
import FileSelectAutoComplete from '../autocomplete/components/FileSelectAutoComplete';
|
||||||
import type { RootState } from './reducer/index';
|
import type { RootState } from './reducer/index';
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { CustomSelectAutoComplete } from '@hydrooj/components';
|
||||||
import {
|
import {
|
||||||
Card, InputGroup, Tag,
|
Card, InputGroup, Tag,
|
||||||
} from '@blueprintjs/core';
|
} from '@blueprintjs/core';
|
||||||
@@ -5,7 +6,6 @@ import { isEqual } from 'lodash';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { i18n } from 'vj/utils';
|
import { i18n } from 'vj/utils';
|
||||||
import CustomSelectAutoComplete from '../autocomplete/components/CustomSelectAutoComplete';
|
|
||||||
import FileSelectAutoComplete from '../autocomplete/components/FileSelectAutoComplete';
|
import FileSelectAutoComplete from '../autocomplete/components/FileSelectAutoComplete';
|
||||||
import { FormItem } from './BasicForm';
|
import { FormItem } from './BasicForm';
|
||||||
import ProblemType from './ProblemType';
|
import ProblemType from './ProblemType';
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { CustomSelectAutoComplete } from '@hydrooj/components';
|
||||||
import { parseMemoryMB, parseTimeMS } from '@hydrooj/utils/lib/common';
|
import { parseMemoryMB, parseTimeMS } from '@hydrooj/utils/lib/common';
|
||||||
import {
|
import {
|
||||||
Button, ControlGroup,
|
Button, ControlGroup,
|
||||||
@@ -8,7 +9,6 @@ import { isEqual } from 'lodash';
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { i18n } from 'vj/utils';
|
import { i18n } from 'vj/utils';
|
||||||
import CustomSelectAutoComplete from '../../autocomplete/components/CustomSelectAutoComplete';
|
|
||||||
import { RootState } from '../reducer';
|
import { RootState } from '../reducer';
|
||||||
|
|
||||||
interface SubtaskSettingsProps {
|
interface SubtaskSettingsProps {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import { readFileSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
import {
|
import {
|
||||||
ContestModel, Context, Handler, ObjectId, param, PERM, PRIV, ProblemModel, Schema,
|
ContestModel, Context, Handler, ObjectId, param, PERM, PRIV, ProblemModel, Schema,
|
||||||
SettingModel, SystemModel, Types, UserModel,
|
SettingModel, SystemModel, Types, UserModel, yaml,
|
||||||
} from 'hydrooj';
|
} from 'hydrooj';
|
||||||
import convert from 'schemastery-jsonschema';
|
import convert from 'schemastery-jsonschema';
|
||||||
import markdown from './backendlib/markdown';
|
import markdown from './backendlib/markdown';
|
||||||
@@ -129,13 +131,58 @@ class RichMediaHandler extends Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fontRange = {
|
||||||
|
'Open Sans': 'Open Sans',
|
||||||
|
'Seravek': 'Seravek',
|
||||||
|
'Segoe UI': 'Segoe UI',
|
||||||
|
'Verdana': 'Verdana',
|
||||||
|
'PingFang SC': 'PingFang SC',
|
||||||
|
'Hiragino Sans GB': 'Hiragino Sans GB',
|
||||||
|
'Microsoft Yahei': 'Microsoft Yahei',
|
||||||
|
'WenQuanYi Micro Hei': 'WenQuanYi Micro Hei',
|
||||||
|
'sans': 'sans',
|
||||||
|
'XiaoLai SC': '小赖 SC',
|
||||||
|
};
|
||||||
|
const codeFontRange = {
|
||||||
|
'monaco': 'Monaco',
|
||||||
|
'Source Code Pro': 'Source Code Pro',
|
||||||
|
'Consolas': 'Consolas',
|
||||||
|
'Lucida Console': 'Lucida Console',
|
||||||
|
'Fira Code': 'Fira Code',
|
||||||
|
'Roboto Mono': 'Roboto Mono',
|
||||||
|
'Inconsolata': 'Inconsolata',
|
||||||
|
'Hack': 'Hack',
|
||||||
|
'Jetbrains Mono': 'Jetbrains Mono',
|
||||||
|
'DM Mono': 'DM Mono',
|
||||||
|
'Ubuntu Mono': 'Ubuntu Mono',
|
||||||
|
'PT Mono': 'PT Mono',
|
||||||
|
'SF Mono': 'SF Mono',
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultAbout = (yaml.load(readFileSync(join(__dirname, 'setting.yaml'), 'utf-8')) as any).about.value;
|
||||||
|
|
||||||
export function apply(ctx: Context) {
|
export function apply(ctx: Context) {
|
||||||
ctx.inject(['setting'], (c) => {
|
ctx.inject(['setting'], (c) => {
|
||||||
c.setting.PreferenceSetting(
|
c.setting.PreferenceSetting(
|
||||||
SettingModel.Setting('setting_display', 'rounded', false, 'boolean', 'Rounded Corners'),
|
SettingModel.Setting('setting_display', 'rounded', false, 'boolean', 'Rounded Corners'),
|
||||||
SettingModel.Setting('setting_display', 'skipAnimate', false, 'boolean', 'Skip Animation'),
|
SettingModel.Setting('setting_display', 'skipAnimate', false, 'boolean', 'Skip Animation'),
|
||||||
SettingModel.Setting('setting_display', 'showTimeAgo', true, 'boolean', 'Enable Time Ago'),
|
SettingModel.Setting('setting_display', 'showTimeAgo', true, 'boolean', 'Enable Time Ago'),
|
||||||
|
SettingModel.Setting('setting_display', 'fontFamily', 'Open Sans', fontRange, 'Font Family'),
|
||||||
|
SettingModel.Setting('setting_display', 'codeFontFamily', 'Source Code Pro', codeFontRange, 'Code Font Family'),
|
||||||
|
SettingModel.Setting('setting_display', 'theme', 'light', { light: 'Light', dark: 'Dark' }, 'Theme'),
|
||||||
|
SettingModel.Setting('setting_markdown', 'preferredEditorType', 'sv', { sv: 'Split View', monaco: 'Monaco Editor' }, 'Preferred Editor Type'),
|
||||||
|
SettingModel.Setting('setting_highlight', 'showInvisibleChar', false, 'boolean', 'Show Invisible Characters'),
|
||||||
|
SettingModel.Setting('setting_highlight', 'formatCode', true, 'boolean', 'Auto Format Code'),
|
||||||
);
|
);
|
||||||
|
c.setting.SystemSetting(Schema.object({
|
||||||
|
'ui-default': Schema.object({
|
||||||
|
footer_extra_html: Schema.string().default(''),
|
||||||
|
nav_logo_dark: Schema.string().default('/components/navigation/nav-logo-small_dark.png'),
|
||||||
|
preload: Schema.string().default(''),
|
||||||
|
domainNavigation: Schema.boolean().default(true).description('Show Domain Navigation'),
|
||||||
|
about: Schema.string().role('markdown').default(defaultAbout),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
if (process.env.HYDRO_CLI) return;
|
if (process.env.HYDRO_CLI) return;
|
||||||
ctx.Route('wiki_help', '/wiki/help', WikiHelpHandler);
|
ctx.Route('wiki_help', '/wiki/help', WikiHelpHandler);
|
||||||
|
|||||||
@@ -99,6 +99,8 @@
|
|||||||
"redux-promise-middleware": "^6.2.0",
|
"redux-promise-middleware": "^6.2.0",
|
||||||
"redux-thunk": "^3.1.0",
|
"redux-thunk": "^3.1.0",
|
||||||
"rupture": "^0.7.1",
|
"rupture": "^0.7.1",
|
||||||
|
"sass": "^1.86.0",
|
||||||
|
"sass-loader": "^16.0.5",
|
||||||
"schemastery": "^3.16.1",
|
"schemastery": "^3.16.1",
|
||||||
"schemastery-react": "^0.1.2",
|
"schemastery-react": "^0.1.2",
|
||||||
"shorty.js": "^1.0.1",
|
"shorty.js": "^1.0.1",
|
||||||
|
|||||||
@@ -1,29 +1,19 @@
|
|||||||
import 'schemastery-react/lib/schemastery-react.css';
|
|
||||||
|
|
||||||
import { diffLines } from 'diff';
|
|
||||||
import yaml from 'js-yaml';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import Schema from 'schemastery';
|
import Schema from 'schemastery';
|
||||||
import Notification from 'vj/components/notification';
|
import Notification from 'vj/components/notification';
|
||||||
|
import DomComponent from 'vj/components/react/DomComponent';
|
||||||
import { NamedPage } from 'vj/misc/Page';
|
import { NamedPage } from 'vj/misc/Page';
|
||||||
import { i18n, request } from 'vj/utils';
|
import { i18n, request } from 'vj/utils';
|
||||||
|
|
||||||
const locales = {
|
|
||||||
zh: 'zh-CN',
|
|
||||||
en: 'en-US',
|
|
||||||
};
|
|
||||||
|
|
||||||
const page = new NamedPage('manage_config', async () => {
|
const page = new NamedPage('manage_config', async () => {
|
||||||
const [{ createSchemasteryReact }, { load }] = await Promise.all([
|
const [{ ConfigEditor, ComponentsProvider }, { load }] = await Promise.all([
|
||||||
import('schemastery-react'),
|
import('@hydrooj/components'),
|
||||||
import('vj/components/monaco/loader'),
|
import('vj/components/monaco/loader'),
|
||||||
]);
|
]);
|
||||||
const { monaco, registerAction, renderMarkdown } = await load(['yaml']);
|
const { monaco, registerAction, renderMarkdown } = await load(['yaml']);
|
||||||
|
|
||||||
const Form = createSchemasteryReact({
|
function Markdown({ source }) {
|
||||||
locale: locales[i18n('__id')] || 'en-US',
|
|
||||||
Markdown({ source }) {
|
|
||||||
const rendered = React.useMemo(() => {
|
const rendered = React.useMemo(() => {
|
||||||
const res = renderMarkdown({ value: source });
|
const res = renderMarkdown({ value: source });
|
||||||
const value = res.element.innerHTML;
|
const value = res.element.innerHTML;
|
||||||
@@ -32,143 +22,38 @@ const page = new NamedPage('manage_config', async () => {
|
|||||||
}, [source]);
|
}, [source]);
|
||||||
// Markdown snippet come from trusted backend code, no need to sanitize here
|
// Markdown snippet come from trusted backend code, no need to sanitize here
|
||||||
return <div dangerouslySetInnerHTML={{ __html: rendered }} />;
|
return <div dangerouslySetInnerHTML={{ __html: rendered }} />;
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
setInterval(() => {
|
|
||||||
monaco.editor.remeasureFonts();
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
const initialConfigString = UiContext.config;
|
|
||||||
const schema = new Schema(UiContext.schema);
|
|
||||||
const initial = yaml.load(UiContext.config);
|
|
||||||
|
|
||||||
function MonacoContainer({ config, setValue, setError }) {
|
|
||||||
const editorRef = React.useRef<HTMLDivElement>(null);
|
|
||||||
const [loading, setLoading] = React.useState(false);
|
|
||||||
const [editor, setEditor] = React.useState<any>(null);
|
|
||||||
const [model, setModel] = React.useState<any>(null);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const listener = () => {
|
|
||||||
if (!editor) return;
|
|
||||||
editor.layout();
|
|
||||||
};
|
|
||||||
window.addEventListener('resize', listener);
|
|
||||||
return () => window.removeEventListener('resize', listener);
|
|
||||||
}, [editor]);
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!editor) return;
|
|
||||||
const disposable = editor.onDidChangeModelContent(() => {
|
|
||||||
const val = editor.getValue({ lineEnding: '\n', preserveBOM: false });
|
|
||||||
try {
|
|
||||||
const loaded = yaml.load(val);
|
|
||||||
schema(loaded);
|
|
||||||
setValue(val);
|
|
||||||
setError('');
|
|
||||||
} catch (err) {
|
|
||||||
setError(err.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return () => disposable.dispose(); // eslint-disable-line
|
|
||||||
}, [editor, setValue, setError]);
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!editorRef.current || loading) return;
|
|
||||||
setLoading(true);
|
|
||||||
// eslint-disable-next-line
|
|
||||||
const model = monaco.editor.createModel(config, 'yaml', monaco.Uri.parse('hydro://system/setting.yaml'));
|
|
||||||
setModel(model);
|
|
||||||
const e = monaco.editor.create(editorRef.current, {
|
|
||||||
theme: 'vs-light',
|
|
||||||
lineNumbers: 'off',
|
|
||||||
glyphMargin: true,
|
|
||||||
lightbulb: { enabled: monaco.editor.ShowLightbulbIconMode.On },
|
|
||||||
model,
|
|
||||||
minimap: { enabled: false },
|
|
||||||
hideCursorInOverviewRuler: true,
|
|
||||||
overviewRulerLanes: 0,
|
|
||||||
overviewRulerBorder: false,
|
|
||||||
fontFamily: UserContext.codeFontFamily,
|
|
||||||
fontLigatures: '',
|
|
||||||
unicodeHighlight: {
|
|
||||||
ambiguousCharacters: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
registerAction(e, model, editorRef.current);
|
|
||||||
setEditor(e);
|
|
||||||
}, [editorRef.current]);
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!editor) return;
|
|
||||||
const current = editor.getValue({ lineEnding: '\n', preserveBOM: false });
|
|
||||||
const diff = diffLines(current, config);
|
|
||||||
const ops = [];
|
|
||||||
let cursor = 1;
|
|
||||||
for (const line of diff) {
|
|
||||||
if (line.added) {
|
|
||||||
let range = model.getFullModelRange();
|
|
||||||
range = range.setStartPosition(cursor, 0);
|
|
||||||
range = range.setEndPosition(cursor, 0);
|
|
||||||
ops.push({ range, text: line.value });
|
|
||||||
} else if (line.removed) {
|
|
||||||
let range = model.getFullModelRange();
|
|
||||||
range = range.setStartPosition(cursor, 0);
|
|
||||||
cursor += line.count;
|
|
||||||
range = range.setEndPosition(cursor, 0);
|
|
||||||
ops.push({ range, text: '' });
|
|
||||||
} else cursor += line.count;
|
|
||||||
}
|
|
||||||
model.pushEditOperations([], ops, undefined);
|
|
||||||
}, [editor, config]);
|
|
||||||
return (
|
|
||||||
<div ref={editorRef} style={{
|
|
||||||
width: '100%', height: '500px',
|
|
||||||
}} />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialClone = JSON.parse(JSON.stringify(initial));
|
ReactDOM.createRoot(document.getElementById('app')!).render(
|
||||||
function App() {
|
<ComponentsProvider theme={UiContext.theme} codeFontFamily={UiContext.codeFontFamily} i18n={i18n}>
|
||||||
const [value, setValue] = React.useState(initialClone);
|
<ConfigEditor
|
||||||
const [info, setInfo] = React.useState('');
|
schema={new Schema(UiContext.schema)}
|
||||||
const [stringConfig, setStringConfig] = React.useState(initialConfigString);
|
config={UiContext.config}
|
||||||
|
monaco={monaco}
|
||||||
const updateFromForm = React.useCallback((v) => {
|
registerAction={registerAction}
|
||||||
const newDump = yaml.dump(v);
|
Markdown={Markdown}
|
||||||
if (newDump === stringConfig) return;
|
onSave={(value) => {
|
||||||
setStringConfig(newDump);
|
request.post('', { value }).then(() => {
|
||||||
setValue(v);
|
|
||||||
}, [stringConfig]);
|
|
||||||
const updateFromMonaco = React.useCallback((v) => {
|
|
||||||
if (v === stringConfig) return;
|
|
||||||
setStringConfig(v);
|
|
||||||
setValue(yaml.load(v));
|
|
||||||
}, [stringConfig]);
|
|
||||||
|
|
||||||
return (<>
|
|
||||||
<div className="row">
|
|
||||||
<div className="medium-5 columns">
|
|
||||||
<MonacoContainer config={stringConfig} setValue={updateFromMonaco} setError={setInfo} />
|
|
||||||
<pre className="help-text">{info}</pre>
|
|
||||||
</div>
|
|
||||||
<div className="medium-7 columns">
|
|
||||||
<Form schema={schema} initial={initial} value={value} onChange={updateFromForm} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="row" style={{ marginTop: '20px' }}>
|
|
||||||
<div className="medium-10 columns">
|
|
||||||
<button onClick={() => {
|
|
||||||
request.post('', { value: stringConfig }).then(() => {
|
|
||||||
Notification.success(i18n('Changes saved successfully'));
|
Notification.success(i18n('Changes saved successfully'));
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
Notification.error(i18n('Failed to save changes:'), e.message);
|
Notification.error(i18n('Failed to save changes:'), e.message);
|
||||||
});
|
});
|
||||||
}} className="rounded primary button">{i18n('Save All Changes')}</button>
|
}}
|
||||||
</div>
|
sidebar={<DomComponent childDom={$('.section.side').get(0)} />}
|
||||||
</div>
|
/>
|
||||||
</>);
|
<style>{`
|
||||||
|
body {
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
.footer {
|
||||||
ReactDOM.createRoot(document.getElementById('app')!).render(<App />);
|
display: hidden;
|
||||||
|
}
|
||||||
|
.omnibar-toggle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</ComponentsProvider>,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default page;
|
export default page;
|
||||||
@@ -1,79 +1,3 @@
|
|||||||
fontFamily:
|
|
||||||
type:
|
|
||||||
Open Sans: Open Sans
|
|
||||||
Seravek: Seravek
|
|
||||||
Segoe UI: Segoe UI
|
|
||||||
Verdana: Verdana
|
|
||||||
PingFang SC: PingFang SC
|
|
||||||
Hiragino Sans GB: Hiragino Sans GB
|
|
||||||
Microsoft Yahei: Microsoft Yahei
|
|
||||||
WenQuanYi Micro Hei: WenQuanYi Micro Hei
|
|
||||||
sans: sans
|
|
||||||
XiaoLai SC: 小赖 SC
|
|
||||||
family: setting_display
|
|
||||||
category: preference
|
|
||||||
value: Open Sans
|
|
||||||
codeFontFamily:
|
|
||||||
type:
|
|
||||||
monaco: Monaco
|
|
||||||
Source Code Pro: Source Code Pro
|
|
||||||
Consolas: Consolas
|
|
||||||
Lucida Console: Lucida Console
|
|
||||||
Fira Code: Fira Code
|
|
||||||
Roboto Mono: Roboto Mono
|
|
||||||
Inconsolata: Inconsolata
|
|
||||||
Hack: Hack
|
|
||||||
Jetbrains Mono: Jetbrains Mono
|
|
||||||
DM Mono: DM Mono
|
|
||||||
Ubuntu Mono: Ubuntu Mono
|
|
||||||
PT Mono: PT Mono
|
|
||||||
SF Mono: SF Mono
|
|
||||||
family: setting_display
|
|
||||||
category: preference
|
|
||||||
value: Source Code Pro
|
|
||||||
footer_extra_html:
|
|
||||||
type: textarea
|
|
||||||
name: footer_extra_html
|
|
||||||
desc: Footer Extra HTML
|
|
||||||
default: ''
|
|
||||||
nav_logo_dark:
|
|
||||||
type: text
|
|
||||||
name: nav_logo_dark
|
|
||||||
desc: Nav Logo Dark
|
|
||||||
default: /components/navigation/nav-logo-small_dark.png
|
|
||||||
preferredEditorType:
|
|
||||||
type:
|
|
||||||
sv: Split View
|
|
||||||
monaco: Monaco Editor
|
|
||||||
family: markdown
|
|
||||||
category: preference
|
|
||||||
value: sv
|
|
||||||
theme:
|
|
||||||
type:
|
|
||||||
light: Light
|
|
||||||
dark: Dark
|
|
||||||
category: preference
|
|
||||||
value: light
|
|
||||||
family: setting_display
|
|
||||||
showInvisibleChar:
|
|
||||||
type: boolean
|
|
||||||
family: highlight
|
|
||||||
desc: show invisible characters when formatting code
|
|
||||||
category: preference
|
|
||||||
formatCode:
|
|
||||||
type: boolean
|
|
||||||
family: highlight
|
|
||||||
desc: Auto format code
|
|
||||||
category: preference
|
|
||||||
value: true
|
|
||||||
preload:
|
|
||||||
type: text
|
|
||||||
default: ''
|
|
||||||
domainNavigation:
|
|
||||||
type: boolean
|
|
||||||
family: setting_display
|
|
||||||
desc: Show Domain Navigation
|
|
||||||
value: true
|
|
||||||
about:
|
about:
|
||||||
type: markdown
|
type: markdown
|
||||||
name: about
|
name: about
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
{% extends "manage_base.html" %}
|
{% extends "manage_base.html" %}
|
||||||
{% block manage_content %}
|
{% block manage_content %}
|
||||||
</div>
|
|
||||||
<div class="medium-12 columns">
|
|
||||||
{{ set(UiContext, 'schema', schema) }}
|
{{ set(UiContext, 'schema', schema) }}
|
||||||
{{ set(UiContext, 'config', value) }}
|
{{ set(UiContext, 'config', value) }}
|
||||||
<div class="section">
|
<div id="app"></div>
|
||||||
<div class="section__body" id="app"></div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Reference in new issue
Block a user