feat: start work on edit/create page

This commit is contained in:
Calum H. 2025-06-10 18:35:01 +01:00
parent 6482d5b465
commit 7eee57eca3
6 changed files with 622 additions and 201 deletions

View File

@ -41,10 +41,12 @@
"@modrinth/ui": "workspace:*",
"@modrinth/utils": "workspace:*",
"@pinia/nuxt": "^0.5.1",
"@types/three": "^0.172.0",
"@vintl/vintl": "^4.4.1",
"@vueuse/core": "^11.1.0",
"ace-builds": "^1.36.2",
"ansi-to-html": "^0.7.2",
"cronstrue": "^2.61.0",
"dayjs": "^1.11.7",
"dompurify": "^3.1.7",
"floating-vue": "^5.2.2",
@ -59,7 +61,6 @@
"qrcode.vue": "^3.4.0",
"semver": "^7.5.4",
"three": "^0.172.0",
"@types/three": "^0.172.0",
"vue-multiselect": "3.0.0-alpha.2",
"vue-typed-virtual-list": "^1.0.10",
"vue3-ace-editor": "^2.2.4",

View File

@ -1,45 +1,45 @@
import type { ScheduledTask } from "@modrinth/utils";
import type { Schedule, ServerSchedule } from "@modrinth/utils";
import { useServersFetch } from "../servers-fetch.ts";
import { ServerModule } from "./base.ts";
export class SchedulingModule extends ServerModule {
tasks: ScheduledTask[] = [];
tasks: ServerSchedule[] = [];
async fetch(): Promise<void> {
// this.tasks = await useServersFetch<ScheduledTask[]>(
// `servers/${this.serverId}/options/schedules`,
// { version: 1 },
// );
const response = await useServersFetch<{ items: ServerSchedule[] }>(
`servers/${this.serverId}/options/schedules`,
{ version: 1 },
);
this.tasks = response.items;
}
async deleteTask(task: ScheduledTask): Promise<void> {
// await useServersFetch(`servers/${this.serverId}/options/schedules`, {
// method: "DELETE",
// body: { title: task.title },
// version: 1,
// });
// this.tasks = this.tasks.filter((t) => t.title !== task.title);
async deleteTask(task: ServerSchedule): Promise<void> {
await useServersFetch(`servers/${this.serverId}/options/schedules/${task.id}`, {
method: "DELETE",
version: 1,
});
this.tasks = this.tasks.filter((t) => t.id !== task.id);
}
async createTask(task: ScheduledTask): Promise<number> {
// await useServersFetch(`servers/${this.serverId}/options/schedules`, {
// method: "POST",
// body: task,
// version: 1,
// });
// this.tasks.push(task);
// return this.tasks.length;
async createTask(task: Schedule): Promise<number> {
const response = await useServersFetch<{ id: number }>(
`servers/${this.serverId}/options/schedules`,
{
method: "POST",
body: task,
version: 1,
},
);
await this.fetch();
return response.id;
}
async editTask(taskTitle: string, updatedTask: Partial<ScheduledTask>): Promise<void> {
// await useServersFetch(`servers/${this.serverId}/options/schedules`, {
// method: "PATCH",
// body: { title: taskTitle, ...updatedTask },
// version: 1,
// });
// const index = this.tasks.findIndex((t) => t.title === taskTitle);
// if (index !== -1) {
// this.tasks[index] = { ...this.tasks[index], ...updatedTask };
// }
async editTask(taskId: number, updatedTask: Partial<Schedule>): Promise<void> {
await useServersFetch(`servers/${this.serverId}/options/schedules/${taskId}`, {
method: "PATCH",
body: updatedTask,
version: 1,
});
await this.fetch();
}
}

View File

@ -0,0 +1,413 @@
<template>
<div class="relative h-full w-full overflow-y-auto">
<div class="flex h-full w-full flex-col">
<div class="p-2 pt-0">
<section v-if="error" class="universal-card">
<h2>An error occurred whilst opening the task editor.</h2>
<p>{{ error }}</p>
<!-- TODO: Replace this with a better error visualization. -->
</section>
<section v-else class="universal-card">
<h2>{{ isNew ? "New scheduled task" : "Editing scheduled task" }}</h2>
<p class="mb-4">
{{
isNew
? "Create a new scheduled task to automatically perform actions at specific times."
: "Modify this scheduled task's settings, timing, and actions."
}}
</p>
<div class="mb-4 flex max-w-md flex-col gap-2">
<label for="title">
<span class="text-lg font-semibold text-contrast">
Title <span class="text-brand-red">*</span>
</span>
</label>
<input
id="title"
v-model="task.title"
type="text"
maxlength="64"
placeholder="Enter task title..."
autocomplete="off"
class="input input-bordered"
required
/>
</div>
<div class="mb-4 flex flex-col gap-2">
<label><h3>Schedule Type</h3></label>
<RadioButtons v-model="scheduleType" :items="['daily', 'custom']" class="mb-2">
<template #default="{ item }">
<span class="flex items-center gap-2">
<ClockIcon v-if="item === 'daily'" class="size-5" />
<CodeIcon v-else class="size-5" />
<span>{{
item === "daily" ? "Every X day(s) at specific time" : "Custom cron expression"
}}</span>
</span>
</template>
</RadioButtons>
</div>
<div
v-if="scheduleType === 'daily'"
class="card mb-4 flex max-w-md flex-row gap-16 rounded-lg !bg-bg p-4"
>
<div class="flex flex-col gap-2">
<label for="dayInterval" class="text-md font-medium">Every X day(s)</label>
<div class="flex items-center gap-2">
<input
id="dayInterval"
v-model="dayInterval"
type="text"
inputmode="numeric"
maxlength="3"
class="input input-bordered w-20"
placeholder="1"
@input="onDayIntervalInput"
/>
<span class="text-muted-foreground text-sm">day(s)</span>
</div>
</div>
<div class="flex flex-col gap-2">
<label class="text-md font-medium">At time</label>
<TimePicker v-model="selectedTime" placeholder="Select time" />
</div>
</div>
<div
v-if="scheduleType === 'custom'"
class="card mb-4 flex max-w-md flex-col gap-2 rounded-lg !bg-bg p-4"
>
<label for="customCron" class="text-md font-medium"
>Cron Expression
<nuxt-link
v-tooltip="
`Click to read more about what cron expressions are, and how to create them.`
"
class="align-middle text-link"
target="_blank"
to="https://www.geeksforgeeks.org/writing-cron-expressions-for-scheduling-tasks/"
><UnknownIcon class="size-4" />
</nuxt-link>
</label>
<input
id="customCron"
v-model="customCron"
type="text"
class="input input-bordered font-mono"
placeholder="0 9 * * *"
/>
<div v-if="!isValidCron" class="text-xs text-brand-red">
Invalid cron format. Please use 5 space-separated values (e.g., minute hour day month
day-of-week).
</div>
<div v-else class="text-xs">
{{ humanReadableDescription }}
</div>
</div>
<div class="mb-4 flex max-w-md flex-col gap-2">
<label for="action_kind"><h3>Action</h3></label>
<RadioButtons v-model="task.action_kind" :items="actionKinds" class="mb-2">
<template #default="{ item }">
<span>{{ item === "restart" ? "Restart server" : "Run game command" }}</span>
</template>
</RadioButtons>
</div>
<div v-if="task.action_kind === 'game-command'" class="mb-4 flex max-w-md flex-col gap-2">
<label for="command"><h3>Command</h3></label>
<input
id="command"
v-model="commandValue"
type="text"
maxlength="256"
placeholder="Enter command to run..."
class="input input-bordered"
/>
</div>
<div class="mb-4 flex max-w-md flex-col gap-2">
<label for="warn_msg"
><h3>Warning Message</h3>
<p>
You can use <code>{}</code> to insert the time until the task is executed.
</p></label
>
<input
id="warn_msg"
v-model="task.warn_msg"
type="text"
maxlength="128"
:placeholder="`/tellraw @a \u0022Restarting in {}!\u0022`"
class="input input-bordered"
/>
<!-- TODO: Warning interval UI -->
</div>
<div class="mt-6 flex max-w-md gap-2">
<ButtonStyled color="brand">
<button :disabled="loading || !isValidCron || !task.title" @click="saveTask">
<SaveIcon />
Save
</button>
</ButtonStyled>
<ButtonStyled>
<button
:disabled="loading"
@click="() => router.push(`/servers/manage/${route.params.id}/options/scheduling`)"
>
<XIcon />
Cancel
</button>
</ButtonStyled>
</div>
</section>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { Schedule, ServerSchedule, ActionKind } from "@modrinth/utils";
import { ref, computed, onBeforeMount, watch } from "vue";
import RadioButtons from "@modrinth/ui/src/components/base/RadioButtons.vue";
import TimePicker from "@modrinth/ui/src/components/base/TimePicker.vue";
import { ButtonStyled } from "@modrinth/ui";
import { ClockIcon, CodeIcon, SaveIcon, XIcon, UnknownIcon } from "@modrinth/assets";
import { toString as cronToString } from "cronstrue";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
import { useRoute, useRouter } from "#imports";
const props = defineProps<{ server: ModrinthServer }>();
const route = useRoute();
const router = useRouter();
const isNew = computed(() => route.params.taskId === "new");
const taskId = computed(() => (isNew.value ? null : Number(route.params.taskId)));
const loading = ref(false);
const error = ref<string | null>(null);
const task = ref<Partial<ServerSchedule | Schedule>>({});
const scheduleType = ref<"daily" | "custom">("daily");
const dayInterval = ref("1");
const selectedTime = ref({ hour: "9", minute: "0" });
const customCron = ref("0 9 * * *");
const actionKinds: ActionKind[] = ["restart", "game-command"];
const commandValue = computed({
get() {
return task.value.options && "command" in task.value.options ? task.value.options.command : "";
},
set(val: string) {
if (task.value.options && "command" in task.value.options) {
task.value.options.command = val;
} else if (task.value.action_kind === "game-command") {
task.value.options = { command: val };
}
},
});
const cronString = computed(() => {
if (scheduleType.value === "custom") {
return customCron.value.trim();
}
const minute = selectedTime.value.minute === "" ? "0" : selectedTime.value.minute;
const hour = selectedTime.value.hour === "" ? "0" : selectedTime.value.hour;
const days = dayInterval.value === "" || Number(dayInterval.value) < 1 ? "1" : dayInterval.value;
if (days === "1") {
return `${minute} ${hour} * * *`;
} else {
return `${minute} ${hour} */${days} * *`;
}
});
const CRON_REGEX =
/^\s*([0-9*/,-]+)\s+([0-9*/,-]+)\s+([0-9*/,-]+)\s+([0-9*/,-]+)\s+([0-9*/,-]+)\s*$/;
const isValidCron = computed(() => {
const cronToTest = scheduleType.value === "custom" ? customCron.value.trim() : cronString.value;
if (!CRON_REGEX.test(cronToTest)) {
return false;
}
if (scheduleType.value === "custom") {
try {
cronToString(cronToTest);
return true;
} catch {
return false;
}
}
return true;
});
const humanReadableDescription = computed<string | undefined>(() => {
if (!isValidCron.value && scheduleType.value === "custom") return undefined;
if (scheduleType.value === "custom") {
try {
return cronToString(customCron.value.trim());
} catch {
return "Invalid cron expression";
}
}
const minute = selectedTime.value.minute === "" ? "0" : selectedTime.value.minute;
const hour = selectedTime.value.hour === "" ? "0" : selectedTime.value.hour;
const daysNum = Number(dayInterval.value || "1");
const timeStr = `${hour.padStart(2, "0")}:${minute.padStart(2, "0")}`;
if (daysNum <= 1) {
return `Every day at ${timeStr}`;
} else {
return `Every ${daysNum} days at ${timeStr}`;
}
});
function onDayIntervalInput(event: Event) {
const input = event.target as HTMLInputElement;
let value = input.value.replace(/\D/g, "");
if (value === "") {
dayInterval.value = "";
return;
}
if (value.length > 1 && value.startsWith("0")) {
value = value.replace(/^0+/, "");
}
const num = parseInt(value);
if (num < 1) {
dayInterval.value = "1";
} else if (num > 365) {
dayInterval.value = "365";
} else {
dayInterval.value = String(num);
}
}
watch([cronString, isValidCron], ([cron, valid]) => {
if (valid) {
task.value.every = cron;
}
});
watch(
() => task.value.action_kind,
(kind) => {
if (kind === "game-command") {
if (
!task.value.options ||
typeof task.value.options !== "object" ||
!("command" in task.value.options)
) {
task.value.options = { command: "" };
}
} else {
task.value.options = {};
}
},
{ immediate: true },
);
onBeforeMount(async () => {
if (!isNew.value && taskId.value != null) {
loading.value = true;
error.value = null;
try {
if (!props.server.scheduling.tasks.length) {
await props.server.scheduling.fetch();
}
const found = props.server.scheduling.tasks.find((t) => t.id === taskId.value);
if (found) {
task.value = { ...found };
if (task.value.every && typeof task.value.every === "string") {
const parts = task.value.every.split(/\s+/);
if (parts.length === 5) {
const minute = parts[0];
const hour = parts[1];
const dayOfMonth = parts[2];
if (dayOfMonth.startsWith("*/")) {
scheduleType.value = "daily";
dayInterval.value = dayOfMonth.substring(2);
selectedTime.value = { hour, minute };
} else if (dayOfMonth === "*" && parts[3] === "*" && parts[4] === "*") {
scheduleType.value = "daily";
dayInterval.value = "1";
selectedTime.value = { hour, minute };
} else {
scheduleType.value = "custom";
customCron.value = task.value.every;
}
} else {
scheduleType.value = "custom";
customCron.value = task.value.every;
}
}
} else {
error.value = "Task not found.";
}
} catch (e) {
error.value = (e as Error).message || "Failed to load task.";
} finally {
loading.value = false;
}
} else {
task.value = {
title: "",
action_kind: "restart",
options: {},
enabled: true,
warn_msg: '/tellraw @a "Restarting in {}!"',
warn_intervals: [],
};
scheduleType.value = "daily";
dayInterval.value = "1";
selectedTime.value = { hour: "9", minute: "0" };
customCron.value = "0 9 * * *";
if (isValidCron.value) {
task.value.every = cronString.value;
}
}
});
async function saveTask() {
loading.value = true;
error.value = null;
if (isValidCron.value) {
task.value.every = cronString.value;
} else {
error.value = "Cannot save with invalid cron expression.";
loading.value = false;
return;
}
try {
if (isNew.value) {
const scheduleToCreate: Schedule = {
title: task.value.title || "",
every: task.value.every!,
action_kind: task.value.action_kind!,
options: task.value.options!,
enabled: task.value.enabled !== undefined ? task.value.enabled : true,
warn_msg: task.value.warn_msg !== undefined ? task.value.warn_msg : "",
warn_intervals: task.value.warn_intervals || [],
};
await props.server.scheduling.createTask(scheduleToCreate);
} else if (taskId.value != null) {
await props.server.scheduling.editTask(taskId.value, task.value as Partial<Schedule>);
}
router.push(`/servers/manage/${route.params.id}/options/scheduling`);
} catch (e) {
error.value = (e as Error).message || "Failed to save task.";
} finally {
loading.value = false;
}
}
</script>

View File

@ -1,164 +1,165 @@
<template>
<div class="relative h-full w-full overflow-y-auto">
<div class="flex h-full w-full flex-col">
<div class="p-2 pt-0">
<section class="universal-card">
<div class="header__row">
<h2 class="header__title text-2xl">Task Scheduling</h2>
<div class="input-group">
<NuxtLink
:to="`/servers/manage/${route.params.id}/options/scheduling/new`"
class="iconified-button brand-button"
>
<PlusIcon />
Create Task
</NuxtLink>
<section class="universal-card">
<div class="mb-6 flex items-center justify-between">
<div class="flex h-full flex-col justify-center">
<h3 class="font-semibold leading-tight">Task Scheduling</h3>
<p v-if="tasks.length < 1" class="mt-1 text-secondary">
No scheduled tasks yet. Click the button to create your first task.
</p>
<p v-else class="mt-1 text-secondary">
You can manage multiple tasks at once by selecting them below.
</p>
</div>
<div>
<NuxtLink
:to="`/servers/manage/${route.params.id}/options/scheduling/new`"
class="iconified-button brand-button"
>
<PlusIcon />
Create Task
</NuxtLink>
</div>
</div>
<template v-if="tasks.length > 0">
<div class="input-group">
<ButtonStyled>
<button :disabled="selectedTasks.length === 0" @click="handleBulkToggle">
<ToggleRightIcon />
Toggle Selected
</button>
</ButtonStyled>
<ButtonStyled color="red">
<button :disabled="selectedTasks.length === 0" @click="handleBulkDelete">
<TrashIcon />
Delete Selected
</button>
</ButtonStyled>
<div class="push-right">
<div class="labeled-control-row">
Sort by
<Multiselect
v-model="sortBy"
:searchable="false"
class="small-select"
:options="['Name', 'Type', 'Enabled', 'Schedule']"
:close-on-select="true"
:show-labels="false"
:allow-empty="false"
@update:model-value="updateSort"
/>
<button
v-tooltip="descending ? 'Descending' : 'Ascending'"
class="square-button"
@click="updateDescending"
>
<DescendingIcon v-if="descending" />
<AscendingIcon v-else />
</button>
</div>
</div>
</div>
<p v-if="tasks.length < 1">
No scheduled tasks yet. Click the button above to create your first task.
</p>
<template v-else>
<p>You can manage multiple tasks at once by selecting them below.</p>
<div class="input-group">
<ButtonStyled>
<button :disabled="selectedTasks.length === 0" @click="handleBulkToggle">
<ToggleRightIcon />
Toggle Selected
</button>
</ButtonStyled>
<ButtonStyled color="red">
<button :disabled="selectedTasks.length === 0" @click="handleBulkDelete">
<TrashIcon />
Delete Selected
</button>
</ButtonStyled>
<div class="push-right">
<div class="labeled-control-row">
Sort by
<Multiselect
v-model="sortBy"
:searchable="false"
class="small-select"
:options="['Name', 'Type', 'Enabled', 'Schedule']"
:close-on-select="true"
:show-labels="false"
:allow-empty="false"
@update:model-value="updateSort"
/>
<button
v-tooltip="descending ? 'Descending' : 'Ascending'"
class="square-button"
@click="updateDescending"
>
<DescendingIcon v-if="descending" />
<AscendingIcon v-else />
</button>
</div>
<div class="grid-table">
<div class="grid-table__row grid-table__header">
<div>
<Checkbox
:model-value="selectedTasks.length === tasks.length && tasks.length > 0"
@update:model-value="handleSelectAll"
/>
</div>
<div>Type</div>
<div>Task Details</div>
<div>Schedule</div>
<div>Warnings</div>
<div>Enabled</div>
<div>Actions</div>
</div>
<div class="grid-table">
<div class="grid-table__row grid-table__header">
<div>
<Checkbox
:model-value="selectedTasks.length === tasks.length && tasks.length > 0"
@update:model-value="handleSelectAll"
/>
</div>
<div>Type</div>
<div>Task Details</div>
<div>Schedule</div>
<div>Warnings</div>
<div>Enabled</div>
<div>Actions</div>
<div
v-for="(task, index) in sortedTasks"
:key="`task-${index}`"
class="grid-table__row"
>
<div>
<Checkbox
:model-value="selectedTasks.includes(task)"
@update:model-value="(value) => handleTaskSelect(task, value)"
/>
</div>
<div
v-for="(task, index) in sortedTasks"
:key="`task-${index}`"
class="grid-table__row"
>
<div>
<Checkbox
:model-value="selectedTasks.includes(task)"
@update:model-value="(value) => handleTaskSelect(task, value)"
/>
</div>
<div>
<RaisedBadge
:text="task.action_kind === 'restart' ? 'Restart' : 'Game Command'"
:icon="task.action_kind === 'restart' ? UpdatedIcon : CodeIcon"
/>
</div>
<div>
<span class="mb-1 block font-medium text-primary">{{ task.title }}</span>
<div
v-if="task.action_kind === 'game-command' && task.options?.command"
class="mt-1"
<div>
<RaisedBadge
:text="task.action_kind === 'restart' ? 'Restart' : 'Game Command'"
:icon="task.action_kind === 'restart' ? UpdatedIcon : CodeIcon"
/>
</div>
<div>
<span class="mb-1 block font-medium text-primary">{{ task.title }}</span>
<div
v-if="task.action_kind === 'game-command' && task.options?.command"
class="mt-1"
>
<code
class="break-all rounded-sm bg-button-bg px-1 py-0.5 text-xs text-secondary"
>
<code
class="break-all rounded-sm bg-button-bg px-1 py-0.5 text-xs text-secondary"
{{ task.options.command }}
</code>
</div>
</div>
<div>
<span class="text-sm text-secondary">{{ getHumanReadableCron(task.every) }}</span>
</div>
<div>
<div
v-if="task.warn_intervals && task.warn_intervals.length > 0"
class="flex flex-col gap-1"
>
<span class="text-sm font-medium text-primary">
{{ task.warn_intervals.length }} warnings
</span>
<div class="font-mono text-xs text-secondary">
{{ formatWarningIntervals(task.warn_intervals) }}
</div>
</div>
<span v-else class="text-sm italic text-secondary">No warnings</span>
</div>
<div>
<Toggle
:model-value="task.enabled"
@update:model-value="(value) => handleTaskToggle(task, value || false)"
/>
</div>
<div>
<div class="flex gap-1">
<ButtonStyled icon-only circular>
<NuxtLink
v-tooltip="'Edit task'"
:to="`/servers/manage/${route.params.id}/options/scheduling/${task.id}`"
>
{{ task.options.command }}
</code>
</div>
</div>
<div>
<span class="text-sm text-secondary">{{ getHumanReadableCron(task.every) }}</span>
</div>
<div>
<div
v-if="task.warn_intervals && task.warn_intervals.length > 0"
class="flex flex-col gap-1"
>
<span class="text-sm font-medium text-primary">
{{ task.warn_intervals.length }} warnings
</span>
<div class="font-mono text-xs text-secondary">
{{ formatWarningIntervals(task.warn_intervals) }}
</div>
</div>
<span v-else class="text-sm italic text-secondary">No warnings</span>
</div>
<div>
<Toggle
:model-value="task.enabled"
@update:model-value="(value) => handleTaskToggle(task, value || false)"
/>
</div>
<div>
<div class="flex gap-1">
<ButtonStyled icon-only circular>
<NuxtLink
v-tooltip="'Edit task'"
:to="`/servers/manage/${route.params.id}/options/scheduling/${encodeURIComponent(task.title)}`"
>
<EditIcon />
</NuxtLink>
</ButtonStyled>
<ButtonStyled icon-only circular color="red">
<button
v-tooltip="
task.title === 'Auto Restart'
? 'You cant delete the automatic restart task.'
: 'Delete task'
"
:disabled="task.title === 'Auto Restart'"
@click="handleTaskDelete(task)"
>
<TrashIcon />
</button>
</ButtonStyled>
</div>
<EditIcon />
</NuxtLink>
</ButtonStyled>
<ButtonStyled icon-only circular color="red">
<button
v-tooltip="
task.title === 'Auto Restart'
? 'You cant delete the automatic restart task.'
: 'Delete task'
"
:disabled="task.title === 'Auto Restart'"
@click="handleTaskDelete(task)"
>
<TrashIcon />
</button>
</ButtonStyled>
</div>
</div>
</div>
</template>
</section>
</div>
</div>
</template>
</section>
</div>
</div>
</template>
@ -178,7 +179,7 @@ import {
} from "@modrinth/assets";
import { Toggle, Checkbox, RaisedBadge, ButtonStyled } from "@modrinth/ui";
import cronstrue from "cronstrue";
import type { ScheduledTask } from "@modrinth/utils";
import type { ServerSchedule } from "@modrinth/utils";
import { ModrinthServer } from "~/composables/servers/modrinth-servers.ts";
const props = defineProps<{
@ -191,11 +192,11 @@ onBeforeMount(async () => {
await props.server.scheduling.fetch();
});
const selectedTasks = ref<ScheduledTask[]>([]);
const selectedTasks = ref<ServerSchedule[]>([]);
const sortBy = ref("Name");
const descending = ref(false);
const tasks = computed(() => props.server.scheduling.tasks);
const tasks = computed(() => props.server.scheduling.tasks as ServerSchedule[]);
const sortedTasks = computed(() => {
const sorted = [...tasks.value];
@ -225,7 +226,7 @@ function handleSelectAll(selected: boolean): void {
selectedTasks.value = selected ? [...tasks.value] : [];
}
function handleTaskSelect(task: ScheduledTask, selected: boolean): void {
function handleTaskSelect(task: ServerSchedule, selected: boolean): void {
if (selected) {
selectedTasks.value.push(task);
} else {
@ -233,17 +234,17 @@ function handleTaskSelect(task: ScheduledTask, selected: boolean): void {
}
}
async function handleTaskToggle(task: ScheduledTask, enabled: boolean): Promise<void> {
async function handleTaskToggle(task: ServerSchedule, enabled: boolean): Promise<void> {
try {
await props.server.scheduling.editTask(task.title, { enabled });
console.log("Toggle task:", task.title, enabled);
await props.server.scheduling.editTask(task.id, { enabled });
console.log("Toggle task:", task.id, enabled);
} catch (error) {
console.error("Failed to toggle task:", error);
task.enabled = !enabled;
}
}
async function handleTaskDelete(task: ScheduledTask): Promise<void> {
async function handleTaskDelete(task: ServerSchedule): Promise<void> {
if (confirm(`Are you sure you want to delete "${task.title}"?`)) {
try {
await props.server.scheduling.deleteTask(task);
@ -262,7 +263,7 @@ async function handleBulkToggle(): Promise<void> {
try {
await Promise.all(
selectedTasks.value.map((task) =>
props.server.scheduling.editTask(task.title, { enabled: shouldEnable }),
props.server.scheduling.editTask(task.id, { enabled: shouldEnable }),
),
);
console.log("Bulk toggle tasks:", selectedTasks.value.length, "to", shouldEnable);

View File

@ -1,10 +1,19 @@
export interface ScheduledTask {
id?: number
export type ActionKind = 'game-command' | 'restart'
export type ScheduleOptions = { command: string } | Record<string, never>
export interface Schedule {
title: string
action_kind: 'game-command' | 'restart'
options: { command?: string }
enabled: boolean
warn_msg?: string
warn_intervals: number[]
every: string
action_kind: ActionKind
options: ScheduleOptions
enabled: boolean
warn_msg: string
warn_intervals: number[]
}
export interface ServerSchedule extends Schedule {
id: number
server_id: string
added_on: string
}

11
pnpm-lock.yaml generated
View File

@ -233,6 +233,9 @@ importers:
ansi-to-html:
specifier: ^0.7.2
version: 0.7.2
cronstrue:
specifier: ^2.61.0
version: 2.61.0
dayjs:
specifier: ^1.11.7
version: 1.11.11
@ -3606,10 +3609,6 @@ packages:
resolution: {integrity: sha512-onMB0OkDjkXunhdW9htFjEhqrD54+M94i6ackoUkjHKbRnXdyEyKRelp4nJ1kAz32+s27jP1FsebpJCVl0BsvA==}
engines: {node: '>=18.0'}
cronstrue@2.52.0:
resolution: {integrity: sha512-NKgHbWkSZXJUcaBHSsyzC8eegD6bBd4O0oCI6XMIJ+y4Bq3v4w7sY3wfWoKPuVlq9pQHRB6od0lmKpIqi8TlKA==}
hasBin: true
cronstrue@2.61.0:
resolution: {integrity: sha512-ootN5bvXbIQI9rW94+QsXN5eROtXWwew6NkdGxIRpS/UFWRggL0G5Al7a9GTBFEsuvVhJ2K3CntIIVt7L2ILhA==}
hasBin: true
@ -9305,7 +9304,7 @@ snapshots:
'@vue/devtools-kit': 7.6.4
birpc: 0.2.19
consola: 3.2.3
cronstrue: 2.52.0
cronstrue: 2.61.0
destr: 2.0.3
error-stack-parser-es: 0.1.5
execa: 7.2.0
@ -11723,8 +11722,6 @@ snapshots:
croner@9.0.0: {}
cronstrue@2.52.0: {}
cronstrue@2.61.0: {}
cross-spawn@6.0.6: