bug fixes

This commit is contained in:
undefined committed 2025-10-23 17:57:29 +08:00
1 parent d0944bdde9
commit 2f2e006eff
10 files changed
+55 -64

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@hydrooj/register", "name": "@hydrooj/register",
"version": "1.0.3", "version": "1.0.4",
"license": "MIT", "license": "MIT",
"main": "index.js", "main": "index.js",
"engines": { "engines": {
+6 -8
View File
@@ -69,21 +69,19 @@ export async function apply(ctx: HydroContext) {
const parallelism = getConfig('parallelism'); const parallelism = getConfig('parallelism');
async function collectInfo() { async function collectInfo() {
const coll = db.collection('status'); const coll = db.collection('status');
const compilers = await compilerVersions(langs); const [compilers, size] = await Promise.all([
compilerVersions(langs),
stackSize(),
]);
await coll.updateOne( await coll.updateOne(
{ mid: info.mid, type: 'server' }, { mid: info.mid, type: 'server' },
{ $set: { compilers } }, { $set: { compilers, stackSize: size } },
{ upsert: true },
);
const size = await stackSize();
await coll.updateOne(
{ mid: info.mid, type: 'server' },
{ $set: { stackSize: size } },
{ upsert: true }, { upsert: true },
); );
} }
ctx.effect(() => { ctx.effect(() => {
const taskConsumer = TaskModel.consume({ type: 'judge' }, handle, true, parallelism); const taskConsumer = TaskModel.consume({ type: 'judge' }, handle, true, parallelism);
collectInfo();
const dispose = ctx.on('system/setting', () => { const dispose = ctx.on('system/setting', () => {
taskConsumer.setConcurrency(getConfig('parallelism')); taskConsumer.setConcurrency(getConfig('parallelism'));
collectInfo(); collectInfo();
+4 -3
View File
@@ -1,6 +1,6 @@
import path from 'path'; import path from 'path';
import { import {
ContestModel, Context, fs, getAlphabeticId, ObjectId, PERM, Schema, STATUS, Types, UserModel, avatar, ContestModel, Context, fs, getAlphabeticId, ObjectId, PERM, Schema, STATUS, Types, UserModel,
} from 'hydrooj'; } from 'hydrooj';
const file = fs.readFileSync(path.join(__dirname, 'public/assets/board.html'), 'utf8'); const file = fs.readFileSync(path.join(__dirname, 'public/assets/board.html'), 'utf8');
@@ -47,8 +47,9 @@ export async function apply(ctx: Context) {
team_id: `${udoc._id}`, team_id: `${udoc._id}`,
name: udoc.uname, name: udoc.uname,
organization: udoc.school, organization: udoc.school,
members: [], members: udoc.members?.split(',').filter((t) => t) || [],
coach: '', coach: udoc.coach,
badge: { url: avatar(udoc.avatar) },
group: [ group: [
...(udoc.group || []), ...(udoc.group || []),
i.unrank ? 'unofficial' : 'official', i.unrank ? 'unofficial' : 'official',
+4 -1
View File
@@ -8,6 +8,9 @@
"postinstall": "node -r @hydrooj/register install.ts" "postinstall": "node -r @hydrooj/register install.ts"
}, },
"dependencies": { "dependencies": {
"@xcpcio/board-app": "^0.64.5" "@xcpcio/board-app": "^0.67.0"
},
"devDependencies": {
"@xcpcio/types": "^0.67.0"
} }
} }
@@ -102,10 +102,13 @@ export function Media(md: MarkdownIt) {
} }
if (service === 'pdf') { if (service === 'pdf') {
if (src.startsWith('file://') || src.startsWith('./')) src += src.includes('?') ? '&noDisposition=1' : '?noDisposition=1'; if (src.startsWith('file://') || src.startsWith('./')) src += src.includes('?') ? '&noDisposition=1' : '?noDisposition=1';
// A response with has content-disposition header causes the browser to download the file automatically.
// As we cannot control response header from external sites, we block embedding external PDFs.
else return '<p>Embedding an external PDF is no longer supported.</p>';
return `\ return `\
<object classid="clsid:${uuid().toUpperCase()}"> <object classid="clsid:${uuid().toUpperCase()}">
<param name="SRC" value="${src}"> <param name="SRC" value="${src}">
<embed width="100%" style="min-height: 100vh;border: none;" fullscreen="yes" src="${src}"> <embed type="application/pdf" width="100%" style="min-height:100vh;border:none;" fullscreen="yes" src="${src}#toolbar=0&navpanes=0">
<noembed></noembed> <noembed></noembed>
</embed> </embed>
</object>`; </object>`;
@@ -125,14 +125,19 @@ export interface Field {
} }
type Result<T extends string, R extends Record<T, Field>> = { type Result<T extends string, R extends Record<T, Field>> = {
[K in keyof R]: ('text' | 'password' | 'username' | 'domain') extends R[K]['type'] ? string [K in keyof R]: R[K]['type'] extends ('text' | 'password' | 'username' | 'domain') ? string
: R[K]['type'] extends 'checkbox' ? boolean : R[K]['type'] extends 'checkbox' ? boolean
: R[K]['type'] extends 'userId' ? number : R[K]['type'] extends 'userId' ? number
: R[K]['type'] extends 'user' ? any : R[K]['type'] extends 'user' ? any
: never; : never;
}; };
export async function prompt<T extends string, R extends Record<T, Field>>(title: string, fields: R): Promise<Result<T, R>> { interface PromptOptions {
cancelByClickingBack: boolean;
cancelByEsc: boolean;
}
export async function prompt<T extends string, R extends Record<T, Field>>(title: string, fields: R, options?: PromptOptions): Promise<Result<T, R>> {
let valueCache: Result<T, R> = {} as any; let valueCache: Result<T, R> = {} as any;
const defaultValues = Object.fromEntries(Object.entries(fields) const defaultValues = Object.fromEntries(Object.entries(fields)
.map(([name, field]: [T, Field]) => { .map(([name, field]: [T, Field]) => {
@@ -225,6 +230,8 @@ export async function prompt<T extends string, R extends Record<T, Field>>(title
const res = await new Dialog({ const res = await new Dialog({
$body: $(div), $body: $(div),
$action: [buttonCancel, buttonOk].join('\n'), $action: [buttonCancel, buttonOk].join('\n'),
cancelByClickingBack: options?.cancelByClickingBack ?? false,
cancelByEsc: options?.cancelByEsc ?? false,
onDispatch(action) { onDispatch(action) {
if (action === 'ok') { if (action === 'ok') {
for (const [name, field] of Object.entries(fields) as [string, Field][]) { for (const [name, field] of Object.entries(fields) as [string, Field][]) {
@@ -2,12 +2,10 @@ import { browserSupportsWebAuthnAutofill, startAuthentication } from '@simpleweb
import $ from 'jquery'; import $ from 'jquery';
import responsiveCutoff from 'vj/breakpoints.json'; import responsiveCutoff from 'vj/breakpoints.json';
import DomDialog from 'vj/components/dialog/DomDialog'; import DomDialog from 'vj/components/dialog/DomDialog';
import { InfoDialog } from 'vj/components/dialog/index'; import { alert } from 'vj/components/dialog/index';
import Notification from 'vj/components/notification'; import Notification from 'vj/components/notification';
import { AutoloadPage } from 'vj/misc/Page'; import { AutoloadPage } from 'vj/misc/Page';
import { import { i18n, mediaQuery, request } from 'vj/utils';
i18n, mediaQuery, request, tpl,
} from 'vj/utils';
const signinDialogPage = new AutoloadPage('signinDialogPage', null, () => { const signinDialogPage = new AutoloadPage('signinDialogPage', null, () => {
const signInDialog = DomDialog.getOrConstruct($('.dialog--signin'), { const signInDialog = DomDialog.getOrConstruct($('.dialog--signin'), {
@@ -18,7 +16,7 @@ const signinDialogPage = new AutoloadPage('signinDialogPage', null, () => {
let authnInitialized = false; let authnInitialized = false;
async function initPasskey() { async function initPasskey() {
if (authnInitialized) return; if (authnInitialized || !window.isSecureContext) return;
authnInitialized = true; authnInitialized = true;
const support = await browserSupportsWebAuthnAutofill(); const support = await browserSupportsWebAuthnAutofill();
if (!support) return; if (!support) return;
@@ -79,9 +77,7 @@ const signinDialogPage = new AutoloadPage('signinDialogPage', null, () => {
$('[data-lostpass]').on('click', (e) => { $('[data-lostpass]').on('click', (e) => {
e.preventDefault(); e.preventDefault();
new InfoDialog({ alert(i18n('Relax and try to remember your password.'));
$body: tpl.typoMsg(i18n('Relax and try to remember your password.')),
}).open();
}); });
}); });
@@ -164,6 +164,9 @@ $highlight-button-color = #F6DF45
.row .row
max-width: 100vw !important; max-width: 100vw !important;
.data-table td
line-height: 1.5
.col--rank .col--rank
width: rem(60px) width: rem(60px)
text-align: center text-align: center
+14 -34
View File
@@ -1,42 +1,22 @@
import $ from 'jquery'; import $ from 'jquery';
import React from 'react'; import { prompt } from 'vj/components/dialog';
import DomainSelectAutoComplete from 'vj/components/autocomplete/DomainSelectAutoComplete';
import { ActionDialog } from 'vj/components/dialog';
import { NamedPage } from 'vj/misc/Page'; import { NamedPage } from 'vj/misc/Page';
import { i18n, tpl } from 'vj/utils'; import { i18n } from 'vj/utils';
export default new NamedPage('home_domain', () => { export default new NamedPage('home_domain', () => {
$(tpl(
<div style={{ display: 'none' }} className="dialog__body--join-domain" id="join-domain-dialog">
<div className="row"><div className="columns">
<h1>{i18n('Join Domain')}</h1>
</div></div>
<div className="row">
<div className="columns">
<label>
{i18n('Domain ID')}
<div className="textbox-container">
<input name="domainId" type="text" className="textbox" data-autofocus />
</div>
</label>
</div>
</div>
</div>,
)).appendTo(document.body);
const selector: any = DomainSelectAutoComplete.getOrConstruct($('#join-domain-dialog [name="domainId"]'));
$(document).on('click', '[id="join-domain-button"]', async () => { $(document).on('click', '[id="join-domain-button"]', async () => {
const action = await new ActionDialog({ const res = await prompt(i18n('Join Domain'), {
$body: $('.dialog__body--join-domain > div'), domain: {
onDispatch(a) { type: 'domain',
if (a === 'ok' && selector.value() === null) { label: i18n('Domain ID'),
selector.focus(); required: true,
return false; autofocus: true,
}
return true;
}, },
}).open(); }, {
if (action !== 'ok') return; cancelByClickingBack: true,
window.location.href = `/domain/join?target=${encodeURIComponent(selector.value()._id.toString())}`; cancelByEsc: true,
});
if (!res?.domain) return;
window.location.href = `/domain/join?target=${encodeURIComponent(res.domain.toString())}`;
}); });
}); });
@@ -15,11 +15,12 @@ import {
addSpeculationRules, delay, i18n, pjax, request, addSpeculationRules, delay, i18n, pjax, request,
} from 'vj/utils'; } from 'vj/utils';
const keywords = ['category', 'difficulty', 'namespace'];
const list = []; const list = [];
const filterTags = {}; const filterTags = {};
const pinned: Record<string, string[]> = { category: [], difficulty: [], namespace: [] }; const pinned: Record<string, string[]> = Object.fromEntries(keywords.map((keyword) => [keyword, []]));
const selections = { category: {}, difficulty: {}, namespace: {} }; const selections = Object.fromEntries(keywords.map((keyword) => [keyword, {}]));
const selectedTags: Record<string, string[]> = { category: [], difficulty: [], namespace: [] }; const selectedTags: Record<string, string[]> = Object.fromEntries(keywords.map((keyword) => [keyword, []]));
let selectedPids: string[] = []; let selectedPids: string[] = [];
let clearSelectionHandler: (() => void) | null = null; let clearSelectionHandler: (() => void) | null = null;
@@ -35,7 +36,7 @@ function setDomSelected($dom, selected, icon?) {
} }
const parserOptions = { const parserOptions = {
keywords: ['category', 'difficulty', 'namespace'], keywords,
offsets: true, offsets: true,
alwaysArray: true, alwaysArray: true,
tokenize: true, tokenize: true,
@@ -46,8 +47,7 @@ function writeSelectionToInput() {
const parsedCurrentValue = parser.parse(currentValue, parserOptions) as SearchParserResult; const parsedCurrentValue = parser.parse(currentValue, parserOptions) as SearchParserResult;
const q = parser.stringify({ const q = parser.stringify({
...parsedCurrentValue, ...parsedCurrentValue,
category: selectedTags.category, ...keywords.reduce((acc, keyword) => ({ ...acc, [keyword]: selectedTags[keyword] }), {}),
difficulty: selectedTags.difficulty,
text: parsedCurrentValue.text, text: parsedCurrentValue.text,
}, parserOptions); }, parserOptions);
$('[name="q"]').val(q); $('[name="q"]').val(q);