Merge remote-tracking branch 'origin/master' into rejudge_task

This commit is contained in:
panda committed 2025-05-20 20:22:07 +00:00
commit 16a4d426a8
96 files changed
+2077 -1691

No files matched your search

+3 -3
View File
@@ -134,7 +134,8 @@ export default async function (env: { watch?: boolean, production?: boolean, mea
extensions: ['.js', '.jsx', '.ts', '.tsx', '.cjs'],
alias: {
vj: root(),
'graphql-ws': root('../../modules/nop.ts'),
'react/jsx-runtime': require.resolve('react/jsx-runtime'),
react: require.resolve('react'),
},
},
module: {
@@ -242,7 +243,7 @@ export default async function (env: { watch?: boolean, production?: boolean, mea
name(module) {
const packageName = module.context.replace(/\\/g, '/').split('node_modules/').pop().split('/')[0];
if (packageName === 'monaco-editor-nls') {
return `i.monaco.${module.userRequest.replace(/\\/g, '/').split('/').pop().split('.')[0]}`;
return `i.monaco.${(module as any).userRequest.replace(/\\/g, '/').split('/').pop().split('.')[0]}`;
}
return `n.${packageName.replace('@', '')}`;
},
@@ -291,7 +292,6 @@ export default async function (env: { watch?: boolean, production?: boolean, mea
{ from: root('components/navigation/nav-logo-small_dark.png'), to: 'components/navigation/nav-logo-small_dark.png' },
{ from: root(`${dirname(require.resolve('streamsaver/package.json'))}/mitm.html`), to: 'streamsaver/mitm.html' },
{ from: root(`${dirname(require.resolve('streamsaver/package.json'))}/sw.js`), to: 'streamsaver/sw.js' },
{ from: root(`${dirname(require.resolve('graphiql/package.json'))}/graphiql.min.css`), to: 'graphiql.min.css' },
{ from: `${dirname(require.resolve('monaco-themes/package.json'))}/themes`, to: 'monaco/themes/' },
],
}),
@@ -22,7 +22,17 @@ const DomainSelectAutoComplete = forwardRef<AutoCompleteHandle<DomainDoc>, AutoC
queryItems={(query) => request.get('/domain/search', { q: query })}
itemText={(domain) => domain.name}
itemKey={(domain) => domain._id}
{...props}
{...{
width: '100%',
height: 'auto',
listStyle: {},
multi: false,
selectedKeys: [],
allowEmptyQuery: true,
freeSolo: false,
freeSoloConverter: (input) => input,
...props,
}}
/>
));
@@ -38,17 +48,6 @@ DomainSelectAutoComplete.propTypes = {
freeSoloConverter: PropTypes.func,
};
DomainSelectAutoComplete.defaultProps = {
width: '100%',
height: 'auto',
listStyle: {},
multi: false,
selectedKeys: [],
allowEmptyQuery: true,
freeSolo: false,
freeSoloConverter: (input) => input,
};
DomainSelectAutoComplete.displayName = 'DomainSelectAutoComplete';
export default DomainSelectAutoComplete;
@@ -19,7 +19,16 @@ const FileSelectAutoComplete = React.forwardRef<any, any>((props, ref) => (
itemKey={(item) => `${item._id?.toString() || item.name || item}`}
renderItem={(item) => item.name || item._id || item} // TODO icon
allowEmptyQuery
{...props}
{...{
width: '100%',
height: 'auto',
listStyle: {},
multi: false,
selectedKeys: [],
freeSolo: false,
freeSoloConverter: (input) => input,
...props,
}}
/>
));
@@ -38,14 +47,4 @@ FileSelectAutoComplete.propTypes = {
FileSelectAutoComplete.displayName = 'FileSelectAutoComplete';
FileSelectAutoComplete.defaultProps = {
width: '100%',
height: 'auto',
listStyle: {},
multi: false,
selectedKeys: [],
freeSolo: false,
freeSoloConverter: (input) => input,
};
export default FileSelectAutoComplete;
@@ -2,7 +2,7 @@ import { AutoComplete, AutoCompleteHandle, AutoCompleteProps } from '@hydrooj/co
import type { ProblemDoc } from 'hydrooj/src/interface';
import PropTypes from 'prop-types';
import React, { forwardRef } from 'react';
import { api, gql, request } from 'vj/utils';
import { api, request } from 'vj/utils';
const ProblemSelectAutoComplete = forwardRef<AutoCompleteHandle<ProblemDoc>, AutoCompleteProps<ProblemDoc>>((props, ref) => (
<AutoComplete<ProblemDoc>
@@ -12,13 +12,7 @@ const ProblemSelectAutoComplete = forwardRef<AutoCompleteHandle<ProblemDoc>, Aut
const { pdocs } = await request.get(`/d/${UiContext.domainId}/p`, { q: query, quick: true });
return pdocs;
}}
fetchItems={(ids) => api(gql`
problems(ids: ${ids.map((i) => +i)}) {
docId
pid
title
}
`, ['data', 'problems'])}
fetchItems={(ids) => api('problems', { ids: ids.map((i) => +i) }, ['docId', 'pid', 'title'])}
itemText={(pdoc) => `${`${pdoc.docId} ${pdoc.title}`}`}
itemKey={(pdoc) => `${pdoc.docId || pdoc}`}
renderItem={(pdoc) => (
@@ -29,7 +23,17 @@ const ProblemSelectAutoComplete = forwardRef<AutoCompleteHandle<ProblemDoc>, Aut
</div>
</div>
)}
{...props}
{...{
width: '100%',
height: 'auto',
listStyle: {},
multi: false,
selectedKeys: [],
allowEmptyQuery: false,
freeSolo: false,
freeSoloConverter: (input) => input,
...props,
}}
/>
));
@@ -45,17 +49,6 @@ ProblemSelectAutoComplete.propTypes = {
freeSoloConverter: PropTypes.func,
};
ProblemSelectAutoComplete.defaultProps = {
width: '100%',
height: 'auto',
listStyle: {},
multi: false,
selectedKeys: [],
allowEmptyQuery: false,
freeSolo: false,
freeSoloConverter: (input) => input,
};
ProblemSelectAutoComplete.displayName = 'ProblemSelectAutoComplete';
export default ProblemSelectAutoComplete;
@@ -2,28 +2,14 @@ import { AutoComplete, AutoCompleteHandle, AutoCompleteProps } from '@hydrooj/co
import type { Udoc } from 'hydrooj/src/interface';
import PropTypes from 'prop-types';
import { forwardRef } from 'react';
import { api, gql } from 'vj/utils';
import { api } from 'vj/utils';
const UserSelectAutoComplete = forwardRef<AutoCompleteHandle<Udoc>, AutoCompleteProps<Udoc>>((props, ref) => (
<AutoComplete<Udoc>
ref={ref as any}
cacheKey="user"
{...props}
queryItems={(query) => api(gql`
users(search: ${query}) {
_id
uname
displayName
avatarUrl
}
`, ['data', 'users'])}
fetchItems={(ids) => api(gql`
users(auto: ${ids}) {
_id
uname
displayName
}
`, ['data', 'users'])}
queryItems={(query) => api('users', { search: query }, ['_id', 'uname', 'displayName', 'avatarUrl'])}
fetchItems={(ids) => api('users', { auto: ids }, ['_id', 'uname', 'displayName'])}
itemText={(user) => user.uname + (user.displayName ? ` (${user.displayName})` : '')}
itemKey={(user) => ((props.multi || /^[+-]?\d+$/.test(user.uname.trim())) ? user._id.toString() : user.uname)}
renderItem={(user) => (
@@ -37,6 +23,17 @@ const UserSelectAutoComplete = forwardRef<AutoCompleteHandle<Udoc>, AutoComplete
</div>
</div>
)}
{...{
width: '100%',
height: 'auto',
listStyle: {},
multi: false,
selectedKeys: [],
allowEmptyQuery: false,
freeSolo: false,
freeSoloConverter: (input) => input,
...props,
}}
/>
));
@@ -52,17 +49,6 @@ UserSelectAutoComplete.propTypes = {
freeSoloConverter: PropTypes.func,
};
UserSelectAutoComplete.defaultProps = {
width: '100%',
height: 'auto',
listStyle: {},
multi: false,
selectedKeys: [],
allowEmptyQuery: false,
freeSolo: false,
freeSoloConverter: (input) => input,
};
UserSelectAutoComplete.displayName = 'UserSelectAutoComplete';
export default UserSelectAutoComplete;
@@ -0,0 +1,88 @@
import { CustomSelectAutoComplete } from '@hydrooj/components';
import $ from 'jquery';
import { AutoloadPage } from 'vj/misc/Page';
import { i18n, tpl } from 'vj/utils';
import { ActionDialog } from './dialog';
async function getAvailableFonts() {
const fonts = await (window as any).queryLocalFonts();
const result = {};
for (const font of fonts) result[font.family] ||= font.fullName;
return Object.entries(result).map(([family, fullName]) => ({
id: family,
name: family === fullName ? family : `${fullName} (${family})`,
fullName,
}));
}
const customFont = localStorage.getItem('customFont');
const customCodeFont = localStorage.getItem('customCodeFont');
export default new AutoloadPage('customFont', (pageName) => {
let dialog;
let selection = '';
let handle = null;
async function showDialog(saveKey: string) {
if (!dialog) {
const fonts = await getAvailableFonts();
dialog = new ActionDialog({
$body: $(tpl(<CustomSelectAutoComplete
ref={(el) => { handle = el; }}
allowEmptyQuery={false}
data={fonts}
renderItem={(item) => <div style={{ fontFamily: item.id }}>{item.name}</div>}
/>, true)),
onDispatch: (action) => {
selection = handle.getSelectedItems()?.[0]?.id;
return action !== 'ok' || selection;
},
cancelByEsc: false,
cancelByClickingBack: false,
});
}
dialog.open().then((action) => {
console.log(action, saveKey, selection);
if (action === 'ok') {
localStorage.setItem(saveKey, selection);
window.location.reload();
} else {
localStorage.removeItem(saveKey);
}
});
}
function FontSelectButton(props: { saveKey: string, label: string, value: string }) {
return <div className="medium-6 columns"><label>
{i18n(props.label)}
<div><button className="button inline" data-show-font-dialog={props.saveKey}>
{props.value ? i18n('Using custom font: {0}', props.value) : i18n('Find custom font')}
</button></div>
</label></div>;
}
if (pageName === 'home_preference' && 'queryLocalFonts' in window) {
$('[name="form_item_fontFamily"]').parent().parent().parent().append($(tpl(
<FontSelectButton saveKey="customFont" label="Select Custom Font" value={customFont} />,
)));
$('[name="form_item_codeFontFamily"]').parent().parent().parent().append($(tpl(
<FontSelectButton saveKey="customCodeFont" label="Select Custom Code Font" value={customCodeFont} />,
)));
$(document).on('click', '[data-show-font-dialog]', (ev) => {
ev.preventDefault();
showDialog(ev.target.dataset.showFontDialog);
});
}
}, () => {
const fontBlock = Array.from(document.head.children).find((el) => el.tagName === 'STYLE' && el.textContent?.includes('--code-font-family'));
if (fontBlock) {
if (customFont) {
fontBlock.textContent = fontBlock.textContent?.replace(/--font-family:/, `--font-family: "${customFont}", `);
}
if (customCodeFont) {
fontBlock.textContent = fontBlock.textContent?.replace(/--code-font-family:/, `--code-font-family: "${customCodeFont}", `);
}
} else {
console.warn('No font style block found');
}
});
@@ -16,6 +16,7 @@ const mapStateToProps = (state) => ({
: null,
});
// eslint-disable-next-line react-refresh/only-export-components
export default connect(mapStateToProps)(class MessagePadDialogueContentContainer extends React.PureComponent {
componentDidMount() {
$(this.refs.list).scrollLock({ strict: true });
@@ -21,6 +21,7 @@ const mapDispatchToProps = (dispatch) => ({
},
});
// eslint-disable-next-line react-refresh/only-export-components
export default connect(mapStateToProps, mapDispatchToProps)(class MessagePadDialogueListContainer extends React.PureComponent {
componentDidMount() {
$(this.refs.list).scrollLock({ strict: true });
@@ -42,6 +42,7 @@ const mapDispatchToProps = (dispatch) => ({
},
});
// eslint-disable-next-line react-refresh/only-export-components
export default connect(mapStateToProps, mapDispatchToProps)(class MessagePadInputContainer extends React.PureComponent {
static contextTypes = {
store: PropTypes.object,
@@ -16,6 +16,7 @@ const mapDispatchToProps = (dispatch) => ({
},
});
// eslint-disable-next-line react-refresh/only-export-components
export default connect(null, mapDispatchToProps)(class MessagePadContainer extends React.PureComponent {
static propTypes = {
onAdd: PropTypes.func.isRequired,
@@ -2,7 +2,7 @@ import keyword from 'emojis-keywords';
import list from 'emojis-list';
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import qface from 'qface';
import { api, gql } from 'vj/utils';
import { api } from 'vj/utils';
function emoji(range) {
return keyword.map((i, index) => ({
@@ -40,12 +40,7 @@ monaco.languages.registerCodeLensProvider('markdown', {
dispose: () => { },
};
}
const { data } = await api(gql`
users(ids: ${users.map((i) => +i.matches[1])}) {
_id
uname
}
`);
const data = await api('users', { ids: users.map((i) => +i.matches[1]) }, ['_id', 'uname']);
return {
lenses: users.map((i, index) => ({
range: i.range,
@@ -53,7 +48,7 @@ monaco.languages.registerCodeLensProvider('markdown', {
command: {
id: 'hydro.openUserPage',
arguments: [i.matches[1]],
title: `@${data.users.find((doc) => doc._id.toString() === i.matches[1])?.uname || i.matches[1]}`,
title: `@${data.find((doc) => doc._id.toString() === i.matches[1])?.uname || i.matches[1]}`,
},
})),
dispose: () => { },
@@ -82,14 +77,7 @@ monaco.languages.registerCompletionItemProvider('markdown', {
endColumn: word.endColumn,
};
if (prefix === '@') {
const users = await api(gql`
users(search: ${word.word}) {
_id
uname
avatarUrl
priv
}
`, ['data', 'users']);
const users = await api('users', { search: word.word }, ['_id', 'uname']);
return {
suggestions: users.map((i) => ({
label: { label: `@${i.uname}`, description: `UID=${i._id}` },
@@ -4,7 +4,7 @@ import ReactDOM from 'react-dom/client';
import { STATUS_CODES } from 'vj/constant/record';
import { AutoloadPage } from 'vj/misc/Page';
import {
api, gql, i18n, request,
api, i18n, request,
} from 'vj/utils';
export default new AutoloadPage('omnibar', () => {
@@ -92,14 +92,7 @@ export default new AutoloadPage('omnibar', () => {
setSearching?.(true);
[{ pdocs, psdict }, udocs] = await Promise.all([
request.get(`/d/${UiContext.domainId}/p`, { q: query, limit: 10 }),
api(gql`
users(search: ${query}) {
_id
uname
displayName
avatarUrl
}
`, ['data', 'users']),
api('users', { search: query }, ['_id', 'uname', 'displayName', 'avatarUrl']),
]);
setSearching?.(false);
}
@@ -39,15 +39,12 @@ function ExtraFilesConfig() {
const userExtraFiles = useSelector((state: RootState) => state.config.user_extra_files || [], isEqual);
const judgeExtraFiles = useSelector((state: RootState) => state.config.judge_extra_files || [], isEqual);
const dispatch = useDispatch();
const userRef = React.useRef<any>();
const judgeRef = React.useRef<any>();
return (
<FormItem columns={12} label="ExtraFilesConfig" disableLabel>
<Card style={{ padding: 10 }}>
<div className="row">
<FormItem columns={12} label={i18n('user_extra_files')}>
<FileSelectAutoComplete
ref={userRef}
data={Files}
selectedKeys={userExtraFiles}
onChange={(val) => dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'user_extra_files', value: val.split(',') })}
@@ -56,7 +53,6 @@ function ExtraFilesConfig() {
</FormItem>
<FormItem columns={12} label={i18n('judge_extra_files')}>
<FileSelectAutoComplete
ref={judgeRef}
data={Files}
selectedKeys={judgeExtraFiles}
onChange={(val) => dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'judge_extra_files', value: val.split(',') })}
@@ -75,7 +71,6 @@ function LangConfig() {
const data = Object.keys(window.LANGS).filter((i) => !prefixes.has(i))
.map((i) => ({ name: `${i.includes('.') ? `${window.LANGS[i.split('.')[0]].display || ''}/` : ''}${window.LANGS[i].display}`, _id: i }));
const dispatch = useDispatch();
const ref = React.useRef<any>();
const selectedKeys = langs.filter((i) => !prefixes.has(i));
return (
<FormItem columns={12} label="langs" disableLabel>
@@ -83,7 +78,6 @@ function LangConfig() {
<div className="row">
<FormItem columns={12} label="langs">
<CustomSelectAutoComplete
ref={ref}
data={data}
placeholder={!selectedKeys.length ? i18n('Unlimited') : i18n('Code language')}
selectedKeys={selectedKeys}
@@ -17,8 +17,8 @@ export function AddTestcase() {
const [autoOutput, setAutoOutput] = React.useState(true);
const testdata = useSelector((state: RootState) => state.testdata);
const store = useStore<RootState>();
const refInput = useRef();
const refOutput = useRef();
const refInput = useRef(null);
const refOutput = useRef(null);
useEffect(() => {
const inputValid = testdata.find((i) => i.name === input);
@@ -3,7 +3,7 @@ import { omit } from 'lodash';
import React from 'react';
export default function DomComponent(props: React.HTMLAttributes<HTMLDivElement> & { childDom: HTMLElement }) {
const ref = React.useRef<HTMLDivElement>();
const ref = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
ref.current.appendChild(props.childDom);
return () => {
@@ -13,6 +13,7 @@ interface ScratchpadOptions {
commandDone?: () => void;
}
// eslint-disable-next-line react-refresh/only-export-components
export default connect((state: any) => ({
value: state.editor.code,
language: window.LANGS[state.editor.lang]?.monaco,
@@ -27,6 +27,7 @@ const mapDispatchToProps = (dispatch) => ({
},
});
// eslint-disable-next-line react-refresh/only-export-components
export default connect(mapStateToProps, mapDispatchToProps)(class ScratchpadPretestContainer extends React.PureComponent {
render() {
return (
@@ -11,6 +11,7 @@ const mapStateToProps = (state) => ({
isLoading: state.ui.records.isLoading,
});
// eslint-disable-next-line react-refresh/only-export-components
export default connect(mapStateToProps)(class ScratchpadRecordsContainer extends React.PureComponent {
render() {
const cn = classNames('data-table is--full-row scratchpad__records__table', {
@@ -46,6 +46,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => ({
data: stateProps.items[ownProps.id],
});
// eslint-disable-next-line react-refresh/only-export-components
export default connect(mapStateToProps, null, mergeProps)(class ScratchpadRecordsRowContainer extends React.PureComponent {
handleRowClick(ev, id) {
const url = substitute(
@@ -73,6 +73,7 @@ const mapDispatchToProps = (dispatch) => ({
const availableLangs = getAvailableLangs(UiContext.pdoc.config.langs);
const keys = Object.keys(availableLangs);
// eslint-disable-next-line react-refresh/only-export-components
export default connect(mapStateToProps, mapDispatchToProps)(class ScratchpadToolbarContainer extends React.PureComponent {
static contextTypes = {
store: PropTypes.object,
@@ -1,9 +1,12 @@
import { browserSupportsWebAuthnAutofill, startAuthentication } from '@simplewebauthn/browser';
import $ from 'jquery';
import responsiveCutoff from 'vj/breakpoints.json';
import DomDialog from 'vj/components/dialog/DomDialog';
import { InfoDialog } from 'vj/components/dialog/index';
import { AutoloadPage } from 'vj/misc/Page';
import { i18n, mediaQuery, tpl } from 'vj/utils';
import {
i18n, mediaQuery, request, tpl,
} from 'vj/utils';
const signinDialogPage = new AutoloadPage('signinDialogPage', null, () => {
const signInDialog = DomDialog.getOrConstruct($('.dialog--signin'), {
@@ -11,13 +14,47 @@ const signinDialogPage = new AutoloadPage('signinDialogPage', null, () => {
cancelByEsc: true,
});
let authnInitialized = false;
async function initPasskey() {
if (authnInitialized) return;
authnInitialized = true;
const support = await browserSupportsWebAuthnAutofill();
if (!support) return;
const authnInfo = await request.get('/user/webauthn', { login: true });
if (!authnInfo.authOptions) return;
const result = await startAuthentication({ optionsJSON: authnInfo.authOptions, useBrowserAutofill: true })
.catch((e) => {
Notification.error(i18n('Failed to get credential: {0}', e));
return null;
});
if (!result) return;
try {
const authn = await request.post('/user/webauthn', {
result,
});
if (authn.url) window.location.href = authn.url;
else if (authn.error) throw new Error(authn.error);
} catch (err) {
Notification.error(err.message);
console.error(err);
}
}
if (window.location.href.endsWith('/login')) initPasskey();
function show() {
signInDialog.show();
initPasskey();
}
// don't show quick login dialog if in mobile
if ($('[name="nav_login"]').length > 0) {
// nav
$('[name="nav_login"]').on('click', (ev) => {
if (mediaQuery.isBelow(responsiveCutoff.mobile)) return;
if (ev.shiftKey || ev.metaKey || ev.ctrlKey) return;
signInDialog.show();
show();
ev.preventDefault();
});
}
@@ -36,7 +73,7 @@ const signinDialogPage = new AutoloadPage('signinDialogPage', null, () => {
return;
}
}
signInDialog.show();
show();
};
$('[data-lostpass]').on('click', (e) => {
@@ -4,7 +4,7 @@ import PQueue from 'p-queue';
import streamsaver from 'streamsaver';
import Notification from 'vj/components/notification';
import {
api, createZipStream, gql, i18n, pipeStream, request,
api, createZipStream, i18n, pipeStream, request,
} from 'vj/utils';
import { ctx } from '../../context';
@@ -94,23 +94,21 @@ export async function downloadProblemSet(pids, name = 'Export') {
try {
await ctx.serial('problemset/download', pids, name, targets);
for (const pid of pids) {
const pdoc = await api(gql`
problem(id: ${+pid}) {
pid
owner
title
content
tag
nSubmit
nAccept
data {
name
}
additional_file {
name
}
}
`, ['data', 'problem']);
const pdoc = await api('problem', { id: +pid }, {
pid: 1,
owner: 1,
title: 1,
content: 1,
tag: 1,
nSubmit: 1,
nAccept: 1,
data: {
name: 1,
},
additional_file: {
name: 1,
},
});
targets.push({
filename: `${pid}/problem.yaml`,
content: dump({
+1 -1
View File
@@ -176,7 +176,7 @@ export function apply(ctx: Context) {
);
c.setting.SystemSetting(Schema.object({
'ui-default': Schema.object({
footer_extra_html: Schema.string().default(''),
footer_extra_html: Schema.string().role('textarea').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'),
+15 -16
View File
@@ -1,6 +1,6 @@
{
"name": "@hydrooj/ui-default",
"version": "4.58.0-beta.0",
"version": "4.58.0-beta.2",
"author": "undefined <i@undefined.moe>",
"license": "AGPL-3.0",
"main": "index.ts",
@@ -11,9 +11,9 @@
"cli": false
},
"devDependencies": {
"@blueprintjs/core": "^5.18.0",
"@blueprintjs/core": "^5.19.0",
"@fontsource/dm-mono": "^5.2.5",
"@fontsource/fira-code": "^5.2.5",
"@fontsource/fira-code": "^5.2.6",
"@fontsource/inconsolata": "^5.2.5",
"@fontsource/jetbrains-mono": "^5.2.5",
"@fontsource/pt-mono": "^5.2.5",
@@ -21,8 +21,8 @@
"@fontsource/source-code-pro": "^5.2.5",
"@fontsource/ubuntu-mono": "^5.2.5",
"@hydrooj/utils": "workspace:^",
"@sentry/browser": "^9.12.0",
"@sentry/webpack-plugin": "^3.3.1",
"@sentry/browser": "^9.17.0",
"@sentry/webpack-plugin": "^3.4.0",
"@simplewebauthn/browser": "13.1.0",
"@svgr/webpack": "^8.1.0",
"@types/html-to-text": "^9.0.4",
@@ -34,8 +34,8 @@
"@types/nunjucks": "^3.2.6",
"@types/pickadate": "^3.5.35",
"@types/qrcode": "^1.5.5",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@types/react": "^19.1.3",
"@types/react-dom": "^19.1.3",
"@types/redux-logger": "^3.0.13",
"@types/uuid": "^10.0.0",
"@types/webpack-env": "^1.18.8",
@@ -49,9 +49,9 @@
"clean-webpack-plugin": "^4.0.0",
"clipboard": "^2.0.11",
"copy-webpack-plugin": "^13.0.0",
"cordis": "4.0.0-beta.1",
"core-js": "^3.41.0",
"core-js-compat": "^3.41.0",
"cordis": "4.0.0-beta.2",
"core-js": "^3.42.0",
"core-js-compat": "^3.42.0",
"css-loader": "^7.1.2",
"diff": "^7.0.0",
"diff-dom": "^5.1.4",
@@ -60,20 +60,19 @@
"emojis-list": "2.1.0",
"esbuild-loader": "^4.3.0",
"flatpickr": "^4.6.13",
"graphiql": "^3.8.3",
"idb": "^8.0.2",
"idb": "^8.0.3",
"jquery": "^3.7.1",
"jquery-scroll-lock": "^3.1.3",
"jquery.easing": "^1.4.1",
"jquery.transit": "^0.9.12",
"matchmedia-polyfill": "^0.3.2",
"md-editor-rt": "^5.4.5",
"md-editor-rt": "^5.5.1",
"mini-css-extract-plugin": "^2.9.2",
"moment": "^2.30.1",
"monaco-editor": "0.52.2",
"monaco-editor-nls": "^3.1.0",
"monaco-editor-webpack-plugin": "^7.1.0",
"monaco-themes": "^0.4.4",
"monaco-themes": "^0.4.5",
"monaco-yaml": "5.1.1",
"nanoid": "^5.1.5",
"normalize.css": "^8.0.1",
@@ -99,7 +98,7 @@
"redux-promise-middleware": "^6.2.0",
"redux-thunk": "^3.1.0",
"rupture": "^0.7.1",
"sass": "^1.86.3",
"sass": "^1.87.0",
"sass-loader": "^16.0.5",
"schemastery": "^3.16.1",
"schemastery-react": "^0.1.2",
@@ -116,7 +115,7 @@
"timeago.js": "^4.0.2",
"ts-loader": "^9.5.2",
"web-streams-polyfill": "^4.1.0",
"webpack": "^5.99.5",
"webpack": "^5.99.8",
"webpack-bundle-analyzer": "^4.10.2",
"webpack-dev-server": "^5.2.1",
"webpack-manifest-plugin": "^5.0.1",
-10
View File
@@ -1,10 +0,0 @@
.graphiql-container .doc-explorer-title, .graphiql-container .history-title
overflow: hidden;
padding: 5px 40px 0px 0px !important
.graphiql-container .docExplorerWrap
font-family: var(--code-font-family) !important
.graphiql-container .doc-explorer-back
overflow: hidden;
padding: 8px 20px 0px 20px !important
-29
View File
@@ -1,29 +0,0 @@
import { pick } from 'lodash';
import React from 'react';
import { createRoot } from 'react-dom/client';
import { NamedPage } from 'vj/misc/Page';
import { request } from 'vj/utils';
const defaultQuery = `\
query Example(
$name: String!
) {
user(uname: $name) {
_id
uname
}
}`;
export default new NamedPage('api', async () => {
const { GraphiQL } = await import('graphiql');
GraphiQL.Logo = function () {
return <p></p>;
} as any;
createRoot(document.getElementById('main')!).render(
<GraphiQL
fetcher={(body, opts = {}) => request.post(`/d/${UiContext.domain._id}/api`, body, pick(opts, 'headers'))}
defaultQuery={defaultQuery}
variables='{"name": "Hydro"}'
/>,
);
});
+9 -12
View File
@@ -5,22 +5,19 @@ import { ActionDialog, ConfirmDialog } from 'vj/components/dialog';
import Notification from 'vj/components/notification';
import { NamedPage } from 'vj/misc/Page';
import {
api, delay, gql, i18n, tpl,
api, delay, i18n, tpl,
} from 'vj/utils';
function update(name: string, uids: number[]) {
return api(gql`
domain { manage { group {
update(name: ${name}, uids: ${uids})
} } }
`);
function update(name: string, uids: number[] = []) {
return api('domain.group', {
name,
uids,
}, []);
}
function del(name: string) {
return api(gql`
domain { manage { group {
del(name: ${name})
} } }
`);
return api('domain.group', {
name,
}, []);
}
const page = new NamedPage('domain_group', () => {
@@ -5,7 +5,7 @@ import VjNotification from 'vj/components/notification';
import selectUser from 'vj/components/selectUser';
import { ctx, Service } from 'vj/context';
import { NamedPage } from 'vj/misc/Page';
import { api, gql, loadReactRedux } from 'vj/utils';
import { api, loadReactRedux } from 'vj/utils';
class MessagePadService extends Service {
WebSocket: typeof import('../components/socket').default;
@@ -91,14 +91,10 @@ const page = new NamedPage('home_messages', () => {
async function loadSendTarget() {
const target = new URL(window.location.href).searchParams.get('target');
if (!target) return;
const user = await api(gql`
users(search: ${target}, exact: true) {
_id
uname
avatarUrl
mail
}
`, ['data', 'users']);
const user = await api(
'users', { search: target, exact: true },
['_id', 'uname', 'avatarUrl', 'mail'],
);
if (!user?.length) return;
createDialog(user[0]);
}
-10
View File
@@ -1,10 +0,0 @@
{% extends "layout/basic.html" %}
{% block content %}
<div style="height: 90vh" id="main"></div>
<style>
#main {
font-family: var(--code-font-family)
}
</style>
<link href="/graphiql.min.css" rel="stylesheet" />
{% endblock %}
@@ -23,7 +23,6 @@
</h1>
<div class="footer__category__expander"><ul class="footer__category__list">
<li class="footer__category__item"><a href="https://github.com/hydro-dev/Hydro" target="_blank">{{ _('Open Source') }}</a></li>
<li class="footer__category__item"><a href="{{ url('api') }}">{{ _('API') }}</a></li>
</ul></div>
</div>
<div class="medium-3 large-2 columns footer__category expandable end">
@@ -20,7 +20,7 @@
<div class="row"><div class="columns">
<label class="material textbox">
{{ _('Username') }}
<input name="uname" type="text" data-autofocus>
<input name="uname" type="text" data-autofocus autocomplete="username webauthn">
</label>
</div></div>
<div class="row"><div class="columns">
+3 -1
View File
@@ -8,7 +8,9 @@
<div class="section__body no-padding">
<div data-fragment-id="ranking">
{% if not udocs.length %}
<blockquote class="note">{{ _('The ranking page is not updated realtime.') }}</blockquote>
{% if page == 1 %}
<blockquote class="note">{{ _('The ranking page is not updated realtime.') }}</blockquote>
{% endif %}
{{ nothing.render('Sorry, there are no users in this domain.') }}
{% else %}
<table class="data-table">
@@ -8,7 +8,7 @@
<div class="row"><div class="columns">
<label class="inverse material textbox">
{{ _('Username') }}
<input name="uname" type="text" autofocus>
<input name="uname" type="text" autofocus autocomplete="username webauthn">
</label>
</div></div>
<div class="row"><div class="columns">
+4 -23
View File
@@ -2,30 +2,11 @@ import 'streamsaver/examples/zip-stream';
import { request } from './base';
export async function api(q: string, path: string[] = []) {
let query = q.trim();
if (!query.startsWith('query')) query = `query{${query}}`;
const res = await request.post(`/d/${UiContext.domainId}/api`, { query });
if (res.errors) throw new Error(res.errors[0].message);
let cursor = res;
for (const p of path) {
cursor = cursor[p];
if (!cursor) return undefined;
}
return cursor;
}
export const gql = (
pieces: TemplateStringsArray,
...templates: (string | number | string[] | number[])[]
) => {
let res = '';
for (let i = 0; i < pieces.length; i++) {
res += pieces[i];
if (templates[i]) res += JSON.stringify(templates[i]);
}
export async function api(method: string, args: Record<string, any>, projection: any) {
const res = await request.post(`/d/${UiContext.domainId}/api/${encodeURIComponent(method)}`, { args, projection });
if (res.error) throw new Error(res.error);
return res;
};
}
export function getAvailableLangs(langsList?: string[]) {
const prefixes = new Set(Object.keys(window.LANGS).filter((i) => i.includes('.')).map((i) => i.split('.')[0]));