题目页「编辑题目」从只给超管放宽到有权改这道题的人(含学生管理员改自己出的题); 题目、比赛、题单列表加「新建」;自学首页加「自学情况」「管理教程」,课文加「编辑这一课」; 公告加「编辑这条」「发公告」;成就加「管理成就」;智能分析加「AI 报告」,看某个学生时 带着用户名过去直接筛到他。按钮统一走 shared/components/AdminLink.vue。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e7b6e27b85
commit
0ffc405a20
12 files changed
+140
-3
No files matched your search
@@ -21,7 +21,13 @@ const theme = useThemeVars()
|
|||||||
|
|
||||||
const reports = ref<AdminAiReportListItem[]>([])
|
const reports = ref<AdminAiReportListItem[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const query = reactive({ limit: 10, page: 1, username: "" })
|
// 前台「智能分析」看某个学生时的「AI 报告」按钮带着 ?username= 过来,直接筛到他
|
||||||
|
const route = useRoute()
|
||||||
|
const query = reactive({
|
||||||
|
limit: 10,
|
||||||
|
page: 1,
|
||||||
|
username: typeof route.query.username === "string" ? route.query.username : "",
|
||||||
|
})
|
||||||
|
|
||||||
const selectedId = ref<number | null>(null)
|
const selectedId = ref<number | null>(null)
|
||||||
const detail = ref<AdminAiReport | null>(null)
|
const detail = ref<AdminAiReport | null>(null)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { getAchievements } from "oj/achievement/api"
|
|||||||
import { getUserBadges } from "oj/api"
|
import { getUserBadges } from "oj/api"
|
||||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||||
import UserName from "shared/components/UserName.vue"
|
import UserName from "shared/components/UserName.vue"
|
||||||
|
import AdminLink from "shared/components/AdminLink.vue"
|
||||||
import { useUserStore } from "shared/store/user"
|
import { useUserStore } from "shared/store/user"
|
||||||
import { RARITY_COLOR, RARITY_LABEL } from "utils/constants"
|
import { RARITY_COLOR, RARITY_LABEL } from "utils/constants"
|
||||||
import type { AchievementList, AchievementRarity, UserBadge } from "utils/types"
|
import type { AchievementList, AchievementRarity, UserBadge } from "utils/types"
|
||||||
@@ -83,6 +84,9 @@ watch(name, load)
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<router-link v-if="other" to="/achievement" class="mine">看我的 ›</router-link>
|
<router-link v-if="other" to="/achievement" class="mine">看我的 ›</router-link>
|
||||||
|
<div v-if="userStore.isSuperAdmin && isDesktop" class="admin-link">
|
||||||
|
<AdminLink to="/admin/achievement/list" size="small">管理成就</AdminLink>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 没有班级(老师、自己注册的号)就没有「本班」,「快拿到了」三个横排占满整宽 -->
|
<!-- 没有班级(老师、自己注册的号)就没有「本班」,「快拿到了」三个横排占满整宽 -->
|
||||||
@@ -168,6 +172,16 @@ h2 {
|
|||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 两个都在时「看我的」占着 auto,按钮跟在它后面 */
|
||||||
|
.admin-link {
|
||||||
|
margin-left: auto;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mine + .admin-link {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.mine {
|
.mine {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
font-size: var(--oj-fs-sec);
|
font-size: var(--oj-fs-sec);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
import { useRouteQuery } from "@vueuse/router"
|
import { useRouteQuery } from "@vueuse/router"
|
||||||
import { useThemeVars } from "naive-ui"
|
import { useThemeVars } from "naive-ui"
|
||||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||||
|
import AdminLink from "shared/components/AdminLink.vue"
|
||||||
import { useUserStore } from "shared/store/user"
|
import { useUserStore } from "shared/store/user"
|
||||||
import UserName from "shared/components/UserName.vue"
|
import UserName from "shared/components/UserName.vue"
|
||||||
import { classLabel } from "oj/submission/utils"
|
import { classLabel } from "oj/submission/utils"
|
||||||
@@ -137,6 +138,13 @@ function goStudent(username: string | null | undefined) {
|
|||||||
>
|
>
|
||||||
看<UserName :username="viewing" />的提交 ›
|
看<UserName :username="viewing" />的提交 ›
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<AdminLink
|
||||||
|
v-if="userStore.isTeacherOrAbove && isDesktop"
|
||||||
|
:to="{ path: '/admin/ai/reports', query: viewing ? { username: viewing } : {} }"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
AI 报告
|
||||||
|
</AdminLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<n-spin :show="loading" :delay="150">
|
<n-spin :show="loading" :delay="150">
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useThemeVars } from "naive-ui"
|
import { useThemeVars } from "naive-ui"
|
||||||
import { getAnnouncement, getAnnouncementList } from "oj/api"
|
import { getAnnouncement, getAnnouncementList } from "oj/api"
|
||||||
|
import AdminLink from "shared/components/AdminLink.vue"
|
||||||
import NotFound from "shared/components/NotFound.vue"
|
import NotFound from "shared/components/NotFound.vue"
|
||||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||||
|
import { useUserStore } from "shared/store/user"
|
||||||
import { errorCode } from "utils/api"
|
import { errorCode } from "utils/api"
|
||||||
import { parseTime, zonedParts, zonedYear } from "utils/functions"
|
import { parseTime, zonedParts, zonedYear } from "utils/functions"
|
||||||
import type { Announcement, AnnouncementListItem } from "utils/types"
|
import type { Announcement, AnnouncementListItem } from "utils/types"
|
||||||
@@ -19,6 +21,7 @@ const LIMIT = 100
|
|||||||
const theme = useThemeVars()
|
const theme = useThemeVars()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const userStore = useUserStore()
|
||||||
const { isDesktop } = useBreakpoints()
|
const { isDesktop } = useBreakpoints()
|
||||||
|
|
||||||
const announcements = ref<AnnouncementListItem[]>([])
|
const announcements = ref<AnnouncementListItem[]>([])
|
||||||
@@ -105,7 +108,19 @@ const showReader = computed(() => isDesktop.value || queryId.value !== null)
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="page oj-page">
|
<div class="page oj-page">
|
||||||
<h2>公告</h2>
|
<div class="head">
|
||||||
|
<h2>公告</h2>
|
||||||
|
<template v-if="userStore.isSuperAdmin">
|
||||||
|
<span class="grow"></span>
|
||||||
|
<AdminLink
|
||||||
|
v-if="showReader && detail"
|
||||||
|
:to="{ name: 'admin announcement edit', params: { announcementID: detail.id } }"
|
||||||
|
>
|
||||||
|
编辑这条
|
||||||
|
</AdminLink>
|
||||||
|
<AdminLink to="/admin/announcement/create">发公告</AdminLink>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
<n-empty v-if="loaded && !announcements.length" description="还没有公告" class="blank" />
|
<n-empty v-if="loaded && !announcements.length" description="还没有公告" class="blank" />
|
||||||
|
|
||||||
@@ -177,6 +192,16 @@ h2 {
|
|||||||
font-size: var(--oj-fs-title);
|
font-size: var(--oj-fs-title);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grow {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.blank {
|
.blank {
|
||||||
margin-top: 60px;
|
margin-top: 60px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import ContestTitle from "shared/components/ContestTitle.vue"
|
|||||||
import Pagination from "shared/components/Pagination.vue"
|
import Pagination from "shared/components/Pagination.vue"
|
||||||
import { useAuthModalStore } from "shared/store/authModal"
|
import { useAuthModalStore } from "shared/store/authModal"
|
||||||
import { usePagination } from "shared/composables/pagination"
|
import { usePagination } from "shared/composables/pagination"
|
||||||
|
import AdminLink from "shared/components/AdminLink.vue"
|
||||||
|
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||||
import { useUserStore } from "shared/store/user"
|
import { useUserStore } from "shared/store/user"
|
||||||
import { ContestStatus } from "utils/constants"
|
import { ContestStatus } from "utils/constants"
|
||||||
import ContestBadge from "./components/ContestBadge.vue"
|
import ContestBadge from "./components/ContestBadge.vue"
|
||||||
@@ -19,6 +21,7 @@ import ContestBadge from "./components/ContestBadge.vue"
|
|||||||
*/
|
*/
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
const { isDesktop } = useBreakpoints()
|
||||||
const authStore = useAuthModalStore()
|
const authStore = useAuthModalStore()
|
||||||
const theme = useThemeVars()
|
const theme = useThemeVars()
|
||||||
const tone = useTone()
|
const tone = useTone()
|
||||||
@@ -145,6 +148,10 @@ const warning = computed(() => tone("warning"))
|
|||||||
{{ item.label }}
|
{{ item.label }}
|
||||||
</button>
|
</button>
|
||||||
<n-input v-model:value="query.keyword" clearable placeholder="比赛名" style="width: 200px" />
|
<n-input v-model:value="query.keyword" clearable placeholder="比赛名" style="width: 200px" />
|
||||||
|
<!-- 手机上一行放不下,也没人在手机上出题,只给电脑 -->
|
||||||
|
<AdminLink v-if="isDesktop && userStore.isTeacherOrAbove" to="/admin/contest/create"
|
||||||
|
>新建比赛</AdminLink
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { TutorialProgress, TutorialSummary } from "@oj2/contract"
|
|||||||
import { getLearnProgress, getTutorials } from "../api"
|
import { getLearnProgress, getTutorials } from "../api"
|
||||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||||
import { useAuthModalStore } from "shared/store/authModal"
|
import { useAuthModalStore } from "shared/store/authModal"
|
||||||
|
import AdminLink from "shared/components/AdminLink.vue"
|
||||||
import { useUserStore } from "shared/store/user"
|
import { useUserStore } from "shared/store/user"
|
||||||
import {
|
import {
|
||||||
COURSES,
|
COURSES,
|
||||||
@@ -251,6 +252,16 @@ onMounted(load)
|
|||||||
>
|
>
|
||||||
登录后记下学到哪
|
登录后记下学到哪
|
||||||
</button>
|
</button>
|
||||||
|
<div v-if="isDesktop && userStore.isTeacherOrAbove" class="admin-links">
|
||||||
|
<AdminLink to="/admin/learn" size="small">自学情况</AdminLink>
|
||||||
|
<AdminLink
|
||||||
|
v-if="userStore.isSuperAdmin"
|
||||||
|
:to="{ path: '/admin/tutorial/list', query: { type: tab } }"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
管理教程
|
||||||
|
</AdminLink>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="current && isDesktop" class="map">
|
<div v-if="current && isDesktop" class="map">
|
||||||
@@ -483,6 +494,11 @@ onMounted(load)
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
.admin-links {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
.login {
|
.login {
|
||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { parseLesson } from "./composables/useLessonParse"
|
|||||||
import { useLearnTrace } from "./composables/useLearnTrace"
|
import { useLearnTrace } from "./composables/useLearnTrace"
|
||||||
import { useBackClose } from "shared/composables/backClose"
|
import { useBackClose } from "shared/composables/backClose"
|
||||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||||
|
import AdminLink from "shared/components/AdminLink.vue"
|
||||||
import { useUserStore } from "shared/store/user"
|
import { useUserStore } from "shared/store/user"
|
||||||
import {
|
import {
|
||||||
COURSES,
|
COURSES,
|
||||||
@@ -293,6 +294,15 @@ function jumpExercise(id: number) {
|
|||||||
<span v-if="tutorial.project">{{ tutorial.project }}</span>
|
<span v-if="tutorial.project">{{ tutorial.project }}</span>
|
||||||
<span v-if="tutorial.project">·</span>
|
<span v-if="tutorial.project">·</span>
|
||||||
<span class="num">第 {{ step }} / {{ titles.length }} 课</span>
|
<span class="num">第 {{ step }} / {{ titles.length }} 课</span>
|
||||||
|
<template v-if="userStore.isSuperAdmin">
|
||||||
|
<span class="grow" />
|
||||||
|
<AdminLink
|
||||||
|
:to="{ name: 'admin tutorial edit', params: { tutorialID: tutorial.id } }"
|
||||||
|
size="tiny"
|
||||||
|
>
|
||||||
|
编辑这一课
|
||||||
|
</AdminLink>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="h1">{{ parts.kp }}</h1>
|
<h1 class="h1">{{ parts.kp }}</h1>
|
||||||
<div v-if="parts.scene" class="scene">{{ parts.scene }}</div>
|
<div v-if="parts.scene" class="scene">{{ parts.scene }}</div>
|
||||||
@@ -546,6 +556,7 @@ function jumpExercise(id: number) {
|
|||||||
}
|
}
|
||||||
.crumb {
|
.crumb {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ export function useEditorMenu(statisticsInline: Ref<boolean>) {
|
|||||||
// 一写回去就顺着 Yjs 同步过去,等于一键清空学生的作业,他还没法撤回
|
// 一写回去就顺着 Yjs 同步过去,等于一键清空学生的作业,他还没法撤回
|
||||||
if (!teacherCollab.value) items.push({ label: "重置代码", key: "reset" })
|
if (!teacherCollab.value) items.push({ label: "重置代码", key: "reset" })
|
||||||
}
|
}
|
||||||
if (isDesktop.value && userStore.isSuperAdmin) items.push({ label: "编辑题目", key: "edit" })
|
if (isDesktop.value && problem.value && userStore.canEditProblem(problem.value))
|
||||||
|
items.push({ label: "编辑题目", key: "edit" })
|
||||||
return items
|
return items
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import Pagination from "shared/components/Pagination.vue"
|
|||||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||||
import { usePagination } from "shared/composables/pagination"
|
import { usePagination } from "shared/composables/pagination"
|
||||||
import { useAuthModalStore } from "shared/store/authModal"
|
import { useAuthModalStore } from "shared/store/authModal"
|
||||||
|
import AdminLink from "shared/components/AdminLink.vue"
|
||||||
import { useUserStore } from "shared/store/user"
|
import { useUserStore } from "shared/store/user"
|
||||||
import ProblemStatus from "./components/ProblemStatus.vue"
|
import ProblemStatus from "./components/ProblemStatus.vue"
|
||||||
import ProblemTypeTag from "./components/ProblemTypeTag.vue"
|
import ProblemTypeTag from "./components/ProblemTypeTag.vue"
|
||||||
@@ -430,6 +431,9 @@ const gridColumns = computed(() =>
|
|||||||
</div>
|
</div>
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
<Hitokoto class="hitokoto" />
|
<Hitokoto class="hitokoto" />
|
||||||
|
<AdminLink v-if="userStore.hasProblemPermission" to="/admin/problem/create" size="small">
|
||||||
|
新建题目
|
||||||
|
</AdminLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="titlebar">
|
<div class="titlebar">
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useThemeVars } from "naive-ui"
|
import { useThemeVars } from "naive-ui"
|
||||||
|
import AdminLink from "shared/components/AdminLink.vue"
|
||||||
|
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||||
|
import { useUserStore } from "shared/store/user"
|
||||||
import { getProblemSetList } from "oj/api"
|
import { getProblemSetList } from "oj/api"
|
||||||
import { rgba, useTone } from "oj/submission/composables/tone"
|
import { rgba, useTone } from "oj/submission/composables/tone"
|
||||||
import { parseTime } from "utils/functions"
|
import { parseTime } from "utils/functions"
|
||||||
@@ -13,6 +16,8 @@ import { badgeCondition, ladder, nextBadge } from "./badges"
|
|||||||
* 题单一共十几个,一次全取回来在前端分组,不分页
|
* 题单一共十几个,一次全取回来在前端分组,不分页
|
||||||
*/
|
*/
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const { isDesktop } = useBreakpoints()
|
||||||
const theme = useThemeVars()
|
const theme = useThemeVars()
|
||||||
const tone = useTone()
|
const tone = useTone()
|
||||||
|
|
||||||
@@ -88,6 +93,10 @@ const doneBackground = computed(() => rgba(theme.value.successColor, 0.05))
|
|||||||
<h2>题单</h2>
|
<h2>题单</h2>
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
<n-input v-model:value="keyword" placeholder="搜题单" clearable class="search" />
|
<n-input v-model:value="keyword" placeholder="搜题单" clearable class="search" />
|
||||||
|
<!-- 手机上一行放不下,也没人在手机上出题,只给电脑 -->
|
||||||
|
<AdminLink v-if="isDesktop && userStore.isTeacherOrAbove" to="/admin/problemset/create"
|
||||||
|
>新建题单</AdminLink
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-if="assigning.length">
|
<template v-if="assigning.length">
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { RouterLink, type RouteLocationRaw } from "vue-router"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 前台页面上直达后台对应功能的按钮(「编辑这一课」「新建比赛」……),和题单页的
|
||||||
|
* 「编辑题单」、比赛页的「比赛设置」同一种样子。渲染成真的 <a>:老师常要一边看前台
|
||||||
|
* 一边改,中键 / Ctrl 点开新标签页得能用。给谁看由调用方用 v-if 管,要和
|
||||||
|
* routes.ts 里目标路由的 meta 对齐,否则点下去被守卫静默踢回首页。
|
||||||
|
*/
|
||||||
|
withDefaults(defineProps<{ to: RouteLocationRaw; size?: "tiny" | "small" | "medium" }>(), {
|
||||||
|
size: "medium",
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<RouterLink v-slot="{ href, navigate }" :to="to" custom>
|
||||||
|
<n-button tag="a" :href="href" :size="size" @click="navigate"><slot /></n-button>
|
||||||
|
</RouterLink>
|
||||||
|
</template>
|
||||||
@@ -49,6 +49,22 @@ export const useUserStore = defineStore("user", () => {
|
|||||||
|
|
||||||
const canToggleDemoMode = computed(() => realIsSuperAdmin.value)
|
const canToggleDemoMode = computed(() => realIsSuperAdmin.value)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 前台「编辑题目」入口给谁看,对齐后端 admin/problem.ts 的 canEdit:
|
||||||
|
* 公开题看题目的创建者(或题目权限是「全部」),比赛题看比赛的创建者。
|
||||||
|
* 题目页拿不到比赛的创建者,比赛题就按题目的创建者估 —— 比赛题基本是出题老师
|
||||||
|
* 自己建或克隆的;估错了后台会拦,只是多一个点进去 404 的入口。
|
||||||
|
*/
|
||||||
|
function canEditProblem(problem: { contestId: number | null; createdBy: { id: number } }) {
|
||||||
|
if (isSuperAdmin.value) return true
|
||||||
|
const mine = problem.createdBy.id === user.value?.id
|
||||||
|
if (problem.contestId !== null) return isTeacherOrAbove.value && mine
|
||||||
|
return (
|
||||||
|
hasProblemPermission.value &&
|
||||||
|
(mine || user.value?.problemPermission === PROBLEM_PERMISSION.ALL)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function toggleDemoMode() {
|
function toggleDemoMode() {
|
||||||
demoSwitch.value = !demoSwitch.value
|
demoSwitch.value = !demoSwitch.value
|
||||||
storage.set(STORAGE_KEY.DEMO_MODE, demoSwitch.value)
|
storage.set(STORAGE_KEY.DEMO_MODE, demoSwitch.value)
|
||||||
@@ -124,6 +140,7 @@ export const useUserStore = defineStore("user", () => {
|
|||||||
hasProblemPermission,
|
hasProblemPermission,
|
||||||
demoMode,
|
demoMode,
|
||||||
canToggleDemoMode,
|
canToggleDemoMode,
|
||||||
|
canEditProblem,
|
||||||
toggleDemoMode,
|
toggleDemoMode,
|
||||||
isAuthed,
|
isAuthed,
|
||||||
showSubmissions,
|
showSubmissions,
|
||||||
|
|||||||
Reference in new issue
Block a user