mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
refactor: clean up client side code for creating schedule (#6805)
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
use tokio::fs;
|
||||
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
@@ -9,13 +10,30 @@ use axum::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::routes::errors::ErrorResponse;
|
||||
use crate::routes::recipe_utils::validate_recipe;
|
||||
use crate::state::AppState;
|
||||
use goose::scheduler::ScheduledJob;
|
||||
use goose::recipe::Recipe;
|
||||
use goose::scheduler::{get_default_scheduled_recipes_dir, ScheduledJob};
|
||||
|
||||
fn validate_schedule_id(id: &str) -> Result<(), ErrorResponse> {
|
||||
let is_valid = !id.is_empty()
|
||||
&& id
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == ' ');
|
||||
|
||||
if !is_valid {
|
||||
return Err(ErrorResponse::bad_request(
|
||||
"Schedule name must use only alphanumeric characters, hyphens, underscores, or spaces"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
|
||||
pub struct CreateScheduleRequest {
|
||||
id: String,
|
||||
recipe_source: String,
|
||||
recipe: Recipe,
|
||||
cron: String,
|
||||
}
|
||||
|
||||
@@ -89,11 +107,36 @@ async fn create_schedule(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<CreateScheduleRequest>,
|
||||
) -> Result<Json<ScheduledJob>, ErrorResponse> {
|
||||
let scheduler = state.scheduler();
|
||||
let id = req.id.trim().to_string();
|
||||
validate_schedule_id(&id)?;
|
||||
|
||||
if req.recipe.check_for_security_warnings() {
|
||||
return Err(ErrorResponse::bad_request(
|
||||
"This recipe contains hidden characters that could be malicious. Please remove them before trying to save.".to_string(),
|
||||
));
|
||||
}
|
||||
if let Err(err) = validate_recipe(&req.recipe) {
|
||||
return Err(ErrorResponse {
|
||||
message: err.message,
|
||||
status: err.status,
|
||||
});
|
||||
}
|
||||
let scheduled_recipes_dir = get_default_scheduled_recipes_dir().map_err(|e| {
|
||||
ErrorResponse::internal(format!("Failed to get scheduled recipes directory: {}", e))
|
||||
})?;
|
||||
|
||||
let recipe_path = scheduled_recipes_dir.join(format!("{}.yaml", id));
|
||||
let yaml_content = req
|
||||
.recipe
|
||||
.to_yaml()
|
||||
.map_err(|e| ErrorResponse::internal(format!("Failed to convert recipe to YAML: {}", e)))?;
|
||||
fs::write(&recipe_path, yaml_content)
|
||||
.await
|
||||
.map_err(|e| ErrorResponse::internal(format!("Failed to save recipe file: {}", e)))?;
|
||||
|
||||
let job = ScheduledJob {
|
||||
id: req.id,
|
||||
source: req.recipe_source,
|
||||
id,
|
||||
source: recipe_path.to_string_lossy().into_owned(),
|
||||
cron: req.cron,
|
||||
last_run: None,
|
||||
currently_running: false,
|
||||
@@ -101,8 +144,10 @@ async fn create_schedule(
|
||||
current_session_id: None,
|
||||
process_start_time: None,
|
||||
};
|
||||
|
||||
let scheduler = state.scheduler();
|
||||
scheduler
|
||||
.add_scheduled_job(job.clone(), true)
|
||||
.add_scheduled_job(job.clone(), false)
|
||||
.await
|
||||
.map_err(|e| match e {
|
||||
goose::scheduler::SchedulerError::CronParseError(msg) => {
|
||||
@@ -117,6 +162,7 @@ async fn create_schedule(
|
||||
},
|
||||
_ => ErrorResponse::internal(format!("Error creating schedule: {}", e)),
|
||||
})?;
|
||||
|
||||
Ok(Json(job))
|
||||
}
|
||||
|
||||
|
||||
@@ -3437,7 +3437,7 @@
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"recipe_source",
|
||||
"recipe",
|
||||
"cron"
|
||||
],
|
||||
"properties": {
|
||||
@@ -3447,8 +3447,8 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"recipe_source": {
|
||||
"type": "string"
|
||||
"recipe": {
|
||||
"$ref": "#/components/schemas/Recipe"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -135,7 +135,7 @@ export type CreateRecipeResponse = {
|
||||
export type CreateScheduleRequest = {
|
||||
cron: string;
|
||||
id: string;
|
||||
recipe_source: string;
|
||||
recipe: Recipe;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,12 +4,11 @@ import { z } from 'zod';
|
||||
import { Download } from 'lucide-react';
|
||||
import { Button } from '../ui/button';
|
||||
import { Input } from '../ui/input';
|
||||
import { Recipe, decodeRecipe } from '../../recipe';
|
||||
import { Recipe, parseDeeplink, parseRecipeFromFile } from '../../recipe';
|
||||
import { toastSuccess, toastError } from '../../toasts';
|
||||
import { useEscapeKey } from '../../hooks/useEscapeKey';
|
||||
import { getRecipeJsonSchema } from '../../recipe/validation';
|
||||
import { saveRecipe } from '../../recipe/recipe_management';
|
||||
import { parseRecipe } from '../../api';
|
||||
import { errorMessage } from '../../utils/conversionUtils';
|
||||
|
||||
interface ImportRecipeFormProps {
|
||||
@@ -46,54 +45,6 @@ export default function ImportRecipeForm({ isOpen, onClose, onSuccess }: ImportR
|
||||
|
||||
useEscapeKey(isOpen, onClose);
|
||||
|
||||
const parseDeeplink = async (deeplink: string): Promise<Recipe | null> => {
|
||||
try {
|
||||
const cleanLink = deeplink.trim();
|
||||
|
||||
if (!cleanLink.startsWith('goose://recipe?config=')) {
|
||||
throw new Error('Invalid deeplink format. Expected: goose://recipe?config=...');
|
||||
}
|
||||
|
||||
const recipeEncoded = cleanLink.replace('goose://recipe?config=', '');
|
||||
|
||||
if (!recipeEncoded) {
|
||||
throw new Error('No recipe configuration found in deeplink');
|
||||
}
|
||||
const recipe = await decodeRecipe(recipeEncoded);
|
||||
|
||||
if (!recipe.title || !recipe.description) {
|
||||
throw new Error('Recipe is missing required fields (title, description)');
|
||||
}
|
||||
|
||||
if (!recipe.instructions && !recipe.prompt) {
|
||||
throw new Error('Recipe must have either instructions or prompt');
|
||||
}
|
||||
|
||||
return recipe;
|
||||
} catch (error) {
|
||||
console.error('Failed to parse deeplink:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const parseRecipeFromFile = async (fileContent: string): Promise<Recipe> => {
|
||||
try {
|
||||
let response = await parseRecipe({
|
||||
body: {
|
||||
content: fileContent,
|
||||
},
|
||||
throwOnError: true,
|
||||
});
|
||||
return response.data.recipe;
|
||||
} catch (error) {
|
||||
let error_message = 'unknown error';
|
||||
if (typeof error === 'object' && error !== null && 'message' in error) {
|
||||
error_message = error.message as string;
|
||||
}
|
||||
throw new Error(error_message);
|
||||
}
|
||||
};
|
||||
|
||||
const importRecipeForm = useForm({
|
||||
defaultValues: {
|
||||
deeplink: '',
|
||||
|
||||
@@ -4,16 +4,14 @@ import { Button } from '../ui/button';
|
||||
import { Input } from '../ui/input';
|
||||
import { ScheduledJob } from '../../schedule';
|
||||
import { CronPicker } from './CronPicker';
|
||||
import { Recipe, decodeRecipe } from '../../recipe';
|
||||
import { Recipe, parseDeeplink, parseRecipeFromFile } from '../../recipe';
|
||||
import { getStorageDirectory } from '../../recipe/recipe_management';
|
||||
import ClockIcon from '../../assets/clock-icon.svg';
|
||||
import * as yaml from 'yaml';
|
||||
|
||||
export interface NewSchedulePayload {
|
||||
id: string;
|
||||
recipe_source: string;
|
||||
recipe: Recipe;
|
||||
cron: string;
|
||||
execution_mode?: string;
|
||||
}
|
||||
|
||||
interface ScheduleModalProps {
|
||||
@@ -28,170 +26,6 @@ interface ScheduleModalProps {
|
||||
|
||||
type SourceType = 'file' | 'deeplink';
|
||||
|
||||
interface CleanExtension {
|
||||
name: string;
|
||||
type: 'stdio' | 'sse' | 'builtin' | 'frontend' | 'streamable_http' | 'platform';
|
||||
cmd?: string;
|
||||
args?: string[];
|
||||
uri?: string;
|
||||
display_name?: string;
|
||||
tools?: unknown[];
|
||||
instructions?: string;
|
||||
env_keys?: string[];
|
||||
timeout?: number;
|
||||
description?: string;
|
||||
bundled?: boolean;
|
||||
}
|
||||
|
||||
interface CleanRecipe {
|
||||
title: string;
|
||||
description: string;
|
||||
instructions?: string;
|
||||
prompt?: string;
|
||||
activities?: string[];
|
||||
extensions?: CleanExtension[];
|
||||
author?: {
|
||||
contact?: string;
|
||||
metadata?: string;
|
||||
};
|
||||
schedule?: {
|
||||
window_title?: string;
|
||||
working_directory?: string;
|
||||
};
|
||||
}
|
||||
|
||||
async function parseDeepLink(deepLink: string): Promise<Recipe | null> {
|
||||
try {
|
||||
const url = new URL(deepLink);
|
||||
if (url.protocol !== 'goose:' || (url.hostname !== 'bot' && url.hostname !== 'recipe')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const recipeParam = url.searchParams.get('config');
|
||||
if (!recipeParam) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await decodeRecipe(recipeParam);
|
||||
} catch (error) {
|
||||
console.error('Failed to parse deep link:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function recipeToYaml(recipe: Recipe): string {
|
||||
const cleanRecipe: CleanRecipe = {
|
||||
title: recipe.title,
|
||||
description: recipe.description,
|
||||
};
|
||||
|
||||
if (recipe.instructions) {
|
||||
cleanRecipe.instructions = recipe.instructions;
|
||||
}
|
||||
|
||||
if (recipe.prompt) {
|
||||
cleanRecipe.prompt = recipe.prompt;
|
||||
}
|
||||
|
||||
if (recipe.activities && recipe.activities.length > 0) {
|
||||
cleanRecipe.activities = recipe.activities;
|
||||
}
|
||||
|
||||
if (recipe.extensions && recipe.extensions.length > 0) {
|
||||
cleanRecipe.extensions = recipe.extensions.map((ext) => {
|
||||
const cleanExt: CleanExtension = {
|
||||
name: ext.name,
|
||||
type: 'builtin',
|
||||
};
|
||||
|
||||
if ('type' in ext && ext.type) {
|
||||
cleanExt.type = ext.type as CleanExtension['type'];
|
||||
|
||||
const extAny = ext as Record<string, unknown>;
|
||||
|
||||
if (ext.type === 'sse' && extAny.uri) {
|
||||
cleanExt.uri = extAny.uri as string;
|
||||
} else if (ext.type === 'streamable_http' && extAny.uri) {
|
||||
cleanExt.uri = extAny.uri as string;
|
||||
} else if (ext.type === 'stdio') {
|
||||
if (extAny.cmd) {
|
||||
cleanExt.cmd = extAny.cmd as string;
|
||||
}
|
||||
if (extAny.args) {
|
||||
cleanExt.args = extAny.args as string[];
|
||||
}
|
||||
} else if ((ext.type === 'builtin' || ext.type === 'platform') && extAny.display_name) {
|
||||
cleanExt.display_name = extAny.display_name as string;
|
||||
}
|
||||
|
||||
if ((ext.type as string) === 'frontend') {
|
||||
if (extAny.tools) {
|
||||
cleanExt.tools = extAny.tools as unknown[];
|
||||
}
|
||||
if (extAny.instructions) {
|
||||
cleanExt.instructions = extAny.instructions as string;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const extAny = ext as Record<string, unknown>;
|
||||
|
||||
if (extAny.cmd) {
|
||||
cleanExt.type = 'stdio';
|
||||
cleanExt.cmd = extAny.cmd as string;
|
||||
if (extAny.args) {
|
||||
cleanExt.args = extAny.args as string[];
|
||||
}
|
||||
} else if (extAny.command) {
|
||||
cleanExt.type = 'stdio';
|
||||
cleanExt.cmd = extAny.command as string;
|
||||
} else if (extAny.uri) {
|
||||
cleanExt.type = 'streamable_http';
|
||||
cleanExt.uri = extAny.uri as string;
|
||||
} else if (extAny.tools) {
|
||||
cleanExt.type = 'frontend';
|
||||
cleanExt.tools = extAny.tools as unknown[];
|
||||
if (extAny.instructions) {
|
||||
cleanExt.instructions = extAny.instructions as string;
|
||||
}
|
||||
} else {
|
||||
cleanExt.type = 'builtin';
|
||||
}
|
||||
}
|
||||
|
||||
if ('env_keys' in ext && ext.env_keys && ext.env_keys.length > 0) {
|
||||
cleanExt.env_keys = ext.env_keys;
|
||||
}
|
||||
|
||||
if ('timeout' in ext && ext.timeout) {
|
||||
cleanExt.timeout = ext.timeout as number;
|
||||
}
|
||||
|
||||
if ('description' in ext && ext.description) {
|
||||
cleanExt.description = ext.description as string;
|
||||
}
|
||||
|
||||
if ('bundled' in ext && ext.bundled !== undefined) {
|
||||
cleanExt.bundled = ext.bundled as boolean;
|
||||
}
|
||||
|
||||
return cleanExt;
|
||||
});
|
||||
}
|
||||
|
||||
if (recipe.author) {
|
||||
cleanRecipe.author = {
|
||||
contact: recipe.author.contact || undefined,
|
||||
metadata: recipe.author.metadata || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
cleanRecipe.schedule = {
|
||||
window_title: `${recipe.title} - Scheduled`,
|
||||
};
|
||||
|
||||
return yaml.stringify(cleanRecipe);
|
||||
}
|
||||
|
||||
const modalLabelClassName = 'block text-sm font-medium text-text-prominent mb-1';
|
||||
|
||||
export const ScheduleModal: React.FC<ScheduleModalProps> = ({
|
||||
@@ -214,33 +48,29 @@ export const ScheduleModal: React.FC<ScheduleModalProps> = ({
|
||||
const [internalValidationError, setInternalValidationError] = useState<string | null>(null);
|
||||
const [isValid, setIsValid] = useState(true);
|
||||
|
||||
const setScheduleIdFromTitle = (title: string) => {
|
||||
const cleanId = title
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9-]/g, '-')
|
||||
.replace(/-+/g, '-');
|
||||
setScheduleId(cleanId);
|
||||
};
|
||||
|
||||
const handleDeepLinkChange = useCallback(async (value: string) => {
|
||||
setDeepLinkInput(value);
|
||||
setInternalValidationError(null);
|
||||
|
||||
if (value.trim()) {
|
||||
try {
|
||||
const recipe = await parseDeepLink(value.trim());
|
||||
if (recipe) {
|
||||
setParsedRecipe(recipe);
|
||||
if (recipe.title) {
|
||||
const cleanId = recipe.title
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9-]/g, '-')
|
||||
.replace(/-+/g, '-');
|
||||
setScheduleId(cleanId);
|
||||
}
|
||||
} else {
|
||||
setParsedRecipe(null);
|
||||
setInternalValidationError(
|
||||
'Invalid deep link format. Please use a goose://bot or goose://recipe link.'
|
||||
);
|
||||
const recipe = await parseDeeplink(value.trim());
|
||||
if (!recipe) throw new Error();
|
||||
setParsedRecipe(recipe);
|
||||
if (recipe.title) {
|
||||
setScheduleIdFromTitle(recipe.title);
|
||||
}
|
||||
} catch {
|
||||
setParsedRecipe(null);
|
||||
setInternalValidationError(
|
||||
'Failed to parse deep link. Please ensure using a goose://bot or goose://recipe link and try again.'
|
||||
);
|
||||
setInternalValidationError('Invalid deep link. Please use a goose://recipe link.');
|
||||
}
|
||||
} else {
|
||||
setParsedRecipe(null);
|
||||
@@ -275,6 +105,26 @@ export const ScheduleModal: React.FC<ScheduleModalProps> = ({
|
||||
if (filePath.endsWith('.yaml') || filePath.endsWith('.yml')) {
|
||||
setRecipeSourcePath(filePath);
|
||||
setInternalValidationError(null);
|
||||
|
||||
try {
|
||||
const fileResponse = await window.electron.readFile(filePath);
|
||||
if (!fileResponse.found || fileResponse.error) {
|
||||
throw new Error('Failed to read the selected file.');
|
||||
}
|
||||
const recipe = await parseRecipeFromFile(fileResponse.file);
|
||||
if (!recipe) {
|
||||
throw new Error('Failed to parse recipe from file.');
|
||||
}
|
||||
setParsedRecipe(recipe);
|
||||
if (recipe.title) {
|
||||
setScheduleIdFromTitle(recipe.title);
|
||||
}
|
||||
} catch (e) {
|
||||
setParsedRecipe(null);
|
||||
setInternalValidationError(
|
||||
e instanceof Error ? e.message : 'Failed to parse recipe from file.'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
setInternalValidationError('Invalid file type: Please select a YAML file (.yaml or .yml)');
|
||||
}
|
||||
@@ -295,47 +145,14 @@ export const ScheduleModal: React.FC<ScheduleModalProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
let finalRecipeSource = '';
|
||||
|
||||
if (sourceType === 'file') {
|
||||
if (!recipeSourcePath) {
|
||||
setInternalValidationError('Recipe source file is required.');
|
||||
return;
|
||||
}
|
||||
finalRecipeSource = recipeSourcePath;
|
||||
} else if (sourceType === 'deeplink') {
|
||||
if (!deepLinkInput.trim()) {
|
||||
setInternalValidationError('Deep link is required.');
|
||||
return;
|
||||
}
|
||||
if (!parsedRecipe) {
|
||||
setInternalValidationError('Invalid deep link. Please check the format.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const yamlContent = recipeToYaml(parsedRecipe);
|
||||
const tempFileName = `schedule-${scheduleId}-${Date.now()}.yaml`;
|
||||
const tempDir = window.electron.getConfig().GOOSE_WORKING_DIR || '.';
|
||||
const tempFilePath = `${tempDir}/${tempFileName}`;
|
||||
|
||||
const writeSuccess = await window.electron.writeFile(tempFilePath, yamlContent);
|
||||
if (!writeSuccess) {
|
||||
setInternalValidationError('Failed to create temporary recipe file.');
|
||||
return;
|
||||
}
|
||||
|
||||
finalRecipeSource = tempFilePath;
|
||||
} catch (error) {
|
||||
console.error('Failed to convert recipe to YAML:', error);
|
||||
setInternalValidationError('Failed to process the recipe from deep link.');
|
||||
return;
|
||||
}
|
||||
if (!parsedRecipe) {
|
||||
setInternalValidationError('Please provide a valid recipe source.');
|
||||
return;
|
||||
}
|
||||
|
||||
const newSchedulePayload: NewSchedulePayload = {
|
||||
id: scheduleId.trim(),
|
||||
recipe_source: finalRecipeSource,
|
||||
recipe: parsedRecipe,
|
||||
cron: cronExpression,
|
||||
};
|
||||
|
||||
@@ -443,7 +260,7 @@ export const ScheduleModal: React.FC<ScheduleModalProps> = ({
|
||||
type="text"
|
||||
value={deepLinkInput}
|
||||
onChange={(e) => handleDeepLinkChange(e.target.value)}
|
||||
placeholder="Paste goose://bot or goose://recipe link here..."
|
||||
placeholder="Paste goose://recipe link here..."
|
||||
className="rounded-full"
|
||||
/>
|
||||
{parsedRecipe && (
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
encodeRecipe as apiEncodeRecipe,
|
||||
decodeRecipe as apiDecodeRecipe,
|
||||
scanRecipe as apiScanRecipe,
|
||||
parseRecipe as apiParseRecipe,
|
||||
} from '../api';
|
||||
import type { RecipeParameter } from '../api';
|
||||
|
||||
@@ -84,3 +85,54 @@ export function stripEmptyExtensions(recipe: Recipe): Recipe {
|
||||
}
|
||||
return recipe;
|
||||
}
|
||||
|
||||
export async function parseRecipeFromFile(fileContent: string): Promise<Recipe> {
|
||||
try {
|
||||
const response = await apiParseRecipe({
|
||||
body: { content: fileContent },
|
||||
throwOnError: true,
|
||||
});
|
||||
|
||||
if (!response.data?.recipe) {
|
||||
throw new Error('No recipe returned from API');
|
||||
}
|
||||
|
||||
return response.data.recipe as Recipe;
|
||||
} catch (error) {
|
||||
let errorMessage = 'unknown error';
|
||||
if (typeof error === 'object' && error !== null && 'message' in error) {
|
||||
errorMessage = error.message as string;
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
export async function parseDeeplink(deeplink: string): Promise<Recipe | null> {
|
||||
try {
|
||||
const cleanLink = deeplink.trim();
|
||||
|
||||
if (!cleanLink.startsWith('goose://recipe?config=')) {
|
||||
throw new Error('Invalid deeplink format. Expected: goose://recipe?config=...');
|
||||
}
|
||||
|
||||
const recipeEncoded = cleanLink.replace('goose://recipe?config=', '');
|
||||
|
||||
if (!recipeEncoded) {
|
||||
throw new Error('No recipe configuration found in deeplink');
|
||||
}
|
||||
const recipe = await decodeRecipe(recipeEncoded);
|
||||
|
||||
if (!recipe.title || !recipe.description) {
|
||||
throw new Error('Recipe is missing required fields (title, description)');
|
||||
}
|
||||
|
||||
if (!recipe.instructions && !recipe.prompt) {
|
||||
throw new Error('Recipe must have either instructions or prompt');
|
||||
}
|
||||
|
||||
return recipe;
|
||||
} catch (error) {
|
||||
console.error('Failed to parse deeplink:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
inspectRunningJob as apiInspectRunningJob,
|
||||
SessionDisplayInfo,
|
||||
} from './api';
|
||||
import type { Recipe } from './api';
|
||||
|
||||
export interface ScheduledJob {
|
||||
id: string;
|
||||
@@ -54,21 +55,15 @@ export async function listSchedules(): Promise<ScheduledJob[]> {
|
||||
|
||||
export async function createSchedule(request: {
|
||||
id: string;
|
||||
recipe_source: string;
|
||||
recipe: Recipe;
|
||||
cron: string;
|
||||
execution_mode?: string;
|
||||
}): Promise<ScheduledJob> {
|
||||
try {
|
||||
const response = await apiCreateSchedule<true>({ body: request });
|
||||
if (response && response.data) {
|
||||
return response.data as ScheduledJob;
|
||||
}
|
||||
console.error('Unexpected response format from apiCreateSchedule', response);
|
||||
throw new Error('Failed to create schedule: Unexpected response format');
|
||||
} catch (error) {
|
||||
console.error('Error creating schedule:', error);
|
||||
throw error;
|
||||
const response = await apiCreateSchedule({ body: request });
|
||||
if (response.data) {
|
||||
return response.data as ScheduledJob;
|
||||
}
|
||||
const err = response.error as { message?: string } | undefined;
|
||||
throw new Error(err?.message || 'Failed to create schedule');
|
||||
}
|
||||
|
||||
export async function deleteSchedule(id: string): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user