mirror of
https://github.com/aljazceru/goose.git
synced 2025-12-19 15:14:21 +01:00
feat: Add schedule creation from deep links with comprehensive extension support (#2738)
This commit is contained in:
@@ -10,6 +10,7 @@ import { FixedExtensionEntry } from './ConfigContext';
|
||||
import RecipeActivityEditor from './RecipeActivityEditor';
|
||||
import RecipeInfoModal from './RecipeInfoModal';
|
||||
import RecipeExpandableInfo from './RecipeExpandableInfo';
|
||||
import { ScheduleFromRecipeModal } from './schedule/ScheduleFromRecipeModal';
|
||||
// import ExtensionList from './settings_v2/extensions/subcomponents/ExtensionList';
|
||||
|
||||
interface RecipeEditorProps {
|
||||
@@ -34,6 +35,7 @@ export default function RecipeEditor({ config }: RecipeEditorProps) {
|
||||
const [extensionsLoaded, setExtensionsLoaded] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [isRecipeInfoModalOpen, setRecipeInfoModalOpen] = useState(false);
|
||||
const [isScheduleModalOpen, setIsScheduleModalOpen] = useState(false);
|
||||
const [recipeInfoModelProps, setRecipeInfoModelProps] = useState<{
|
||||
label: string;
|
||||
value: string;
|
||||
@@ -331,6 +333,13 @@ export default function RecipeEditor({ config }: RecipeEditorProps) {
|
||||
</div>
|
||||
{/* Action Buttons */}
|
||||
<div className="flex flex-col space-y-2 pt-1">
|
||||
<button
|
||||
onClick={() => setIsScheduleModalOpen(true)}
|
||||
disabled={!requiredFieldsAreFilled()}
|
||||
className="w-full p-3 bg-green-500 text-white rounded-lg hover:bg-green-600 disabled:opacity-50 disabled:hover:bg-green-500"
|
||||
>
|
||||
Create Schedule from Recipe
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
localStorage.removeItem('recipe_editor_extensions');
|
||||
@@ -350,6 +359,17 @@ export default function RecipeEditor({ config }: RecipeEditorProps) {
|
||||
onClose={() => setRecipeInfoModalOpen(false)}
|
||||
onSaveValue={recipeInfoModelProps?.setValue}
|
||||
/>
|
||||
<ScheduleFromRecipeModal
|
||||
isOpen={isScheduleModalOpen}
|
||||
onClose={() => setIsScheduleModalOpen(false)}
|
||||
recipe={getCurrentConfig()}
|
||||
onCreateSchedule={(deepLink) => {
|
||||
// Open the schedules view with the deep link pre-filled
|
||||
window.electron.createChatWindow(undefined, undefined, undefined, undefined, undefined, 'schedules');
|
||||
// Store the deep link in localStorage for the schedules view to pick up
|
||||
localStorage.setItem('pendingScheduleDeepLink', deepLink);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import React, { useState, useEffect, FormEvent } from 'react';
|
||||
import React, { useState, useEffect, FormEvent, useCallback } from 'react';
|
||||
import { Card } from '../ui/card';
|
||||
import { Button } from '../ui/button';
|
||||
import { Input } from '../ui/input';
|
||||
import { Select } from '../ui/Select';
|
||||
import cronstrue from 'cronstrue';
|
||||
import * as yaml from 'yaml';
|
||||
import { Buffer } from 'buffer';
|
||||
import { Recipe } from '../../recipe';
|
||||
|
||||
type FrequencyValue = 'once' | 'hourly' | 'daily' | 'weekly' | 'monthly';
|
||||
|
||||
@@ -26,6 +29,39 @@ interface CreateScheduleModalProps {
|
||||
apiErrorExternally: string | null;
|
||||
}
|
||||
|
||||
// Interface for clean extension in YAML
|
||||
interface CleanExtension {
|
||||
name: string;
|
||||
type: 'stdio' | 'sse' | 'builtin' | 'frontend';
|
||||
cmd?: string;
|
||||
args?: string[];
|
||||
uri?: string;
|
||||
display_name?: string;
|
||||
tools?: unknown[];
|
||||
instructions?: string;
|
||||
env_keys?: string[];
|
||||
timeout?: number;
|
||||
description?: string;
|
||||
bundled?: boolean;
|
||||
}
|
||||
|
||||
// Interface for clean recipe in YAML
|
||||
interface CleanRecipe {
|
||||
title: string;
|
||||
description: string;
|
||||
instructions: string;
|
||||
prompt?: string;
|
||||
activities?: string[];
|
||||
extensions?: CleanExtension[];
|
||||
goosehints?: string;
|
||||
context?: string[];
|
||||
profile?: string;
|
||||
author?: {
|
||||
contact?: string;
|
||||
metadata?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const frequencies: FrequencyOption[] = [
|
||||
{ value: 'once', label: 'Once' },
|
||||
{ value: 'hourly', label: 'Hourly' },
|
||||
@@ -51,6 +87,153 @@ const checkboxLabelClassName = 'flex items-center text-sm text-textStandard dark
|
||||
const checkboxInputClassName =
|
||||
'h-4 w-4 text-indigo-600 border-gray-300 dark:border-gray-600 rounded focus:ring-indigo-500 mr-2';
|
||||
|
||||
type SourceType = 'file' | 'deeplink';
|
||||
|
||||
// Function to parse deep link and extract recipe config
|
||||
function parseDeepLink(deepLink: string): Recipe | null {
|
||||
try {
|
||||
const url = new URL(deepLink);
|
||||
if (url.protocol !== 'goose:' || (url.hostname !== 'bot' && url.hostname !== 'recipe')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const configParam = url.searchParams.get('config');
|
||||
if (!configParam) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const configJson = Buffer.from(configParam, 'base64').toString('utf-8');
|
||||
return JSON.parse(configJson) as Recipe;
|
||||
} catch (error) {
|
||||
console.error('Failed to parse deep link:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to convert recipe to YAML
|
||||
function recipeToYaml(recipe: Recipe): string {
|
||||
// Create a clean recipe object for YAML conversion
|
||||
const cleanRecipe: CleanRecipe = {
|
||||
title: recipe.title,
|
||||
description: recipe.description,
|
||||
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', // Default type, will be overridden below
|
||||
};
|
||||
|
||||
// Handle different extension types
|
||||
if ('type' in ext && ext.type) {
|
||||
cleanExt.type = ext.type as CleanExtension['type'];
|
||||
|
||||
// Add type-specific fields based on the ExtensionConfig union types
|
||||
switch (ext.type) {
|
||||
case 'sse':
|
||||
if ('uri' in ext && ext.uri) {
|
||||
cleanExt.uri = ext.uri as string;
|
||||
}
|
||||
break;
|
||||
case 'stdio':
|
||||
if ('cmd' in ext && ext.cmd) {
|
||||
cleanExt.cmd = ext.cmd as string;
|
||||
}
|
||||
if ('args' in ext && ext.args) {
|
||||
cleanExt.args = ext.args as string[];
|
||||
}
|
||||
break;
|
||||
case 'builtin':
|
||||
if ('display_name' in ext && ext.display_name) {
|
||||
cleanExt.display_name = ext.display_name as string;
|
||||
}
|
||||
break;
|
||||
case 'frontend':
|
||||
if ('tools' in ext && ext.tools) {
|
||||
cleanExt.tools = ext.tools as unknown[];
|
||||
}
|
||||
if ('instructions' in ext && ext.instructions) {
|
||||
cleanExt.instructions = ext.instructions as string;
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Fallback: try to infer type from available fields
|
||||
if ('cmd' in ext && ext.cmd) {
|
||||
cleanExt.type = 'stdio';
|
||||
cleanExt.cmd = ext.cmd as string;
|
||||
if ('args' in ext && ext.args) {
|
||||
cleanExt.args = ext.args as string[];
|
||||
}
|
||||
} else if ('command' in ext && ext.command) {
|
||||
// Handle legacy 'command' field by converting to 'cmd'
|
||||
cleanExt.type = 'stdio';
|
||||
cleanExt.cmd = ext.command as string;
|
||||
} else if ('uri' in ext && ext.uri) {
|
||||
cleanExt.type = 'sse';
|
||||
cleanExt.uri = ext.uri as string;
|
||||
} else if ('tools' in ext && ext.tools) {
|
||||
cleanExt.type = 'frontend';
|
||||
cleanExt.tools = ext.tools as unknown[];
|
||||
if ('instructions' in ext && ext.instructions) {
|
||||
cleanExt.instructions = ext.instructions as string;
|
||||
}
|
||||
} else {
|
||||
// Default to builtin if we can't determine type
|
||||
cleanExt.type = 'builtin';
|
||||
}
|
||||
}
|
||||
|
||||
// Add common optional fields
|
||||
if (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.goosehints) {
|
||||
cleanRecipe.goosehints = recipe.goosehints;
|
||||
}
|
||||
|
||||
if (recipe.context && recipe.context.length > 0) {
|
||||
cleanRecipe.context = recipe.context;
|
||||
}
|
||||
|
||||
if (recipe.profile) {
|
||||
cleanRecipe.profile = recipe.profile;
|
||||
}
|
||||
|
||||
if (recipe.author) {
|
||||
cleanRecipe.author = recipe.author;
|
||||
}
|
||||
|
||||
return yaml.stringify(cleanRecipe);
|
||||
}
|
||||
|
||||
export const CreateScheduleModal: React.FC<CreateScheduleModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
@@ -59,7 +242,10 @@ export const CreateScheduleModal: React.FC<CreateScheduleModalProps> = ({
|
||||
apiErrorExternally,
|
||||
}) => {
|
||||
const [scheduleId, setScheduleId] = useState<string>('');
|
||||
const [sourceType, setSourceType] = useState<SourceType>('file');
|
||||
const [recipeSourcePath, setRecipeSourcePath] = useState<string>('');
|
||||
const [deepLinkInput, setDeepLinkInput] = useState<string>('');
|
||||
const [parsedRecipe, setParsedRecipe] = useState<Recipe | null>(null);
|
||||
const [frequency, setFrequency] = useState<FrequencyValue>('daily');
|
||||
const [selectedDate, setSelectedDate] = useState<string>(
|
||||
() => new Date().toISOString().split('T')[0]
|
||||
@@ -72,9 +258,46 @@ export const CreateScheduleModal: React.FC<CreateScheduleModalProps> = ({
|
||||
const [readableCronExpression, setReadableCronExpression] = useState<string>('');
|
||||
const [internalValidationError, setInternalValidationError] = useState<string | null>(null);
|
||||
|
||||
const handleDeepLinkChange = useCallback((value: string) => {
|
||||
setDeepLinkInput(value);
|
||||
setInternalValidationError(null);
|
||||
|
||||
if (value.trim()) {
|
||||
const recipe = parseDeepLink(value.trim());
|
||||
if (recipe) {
|
||||
setParsedRecipe(recipe);
|
||||
// Auto-populate schedule ID from recipe title if available
|
||||
if (recipe.title && !scheduleId) {
|
||||
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.');
|
||||
}
|
||||
} else {
|
||||
setParsedRecipe(null);
|
||||
}
|
||||
}, [scheduleId]);
|
||||
|
||||
useEffect(() => {
|
||||
// Check for pending deep link when modal opens
|
||||
if (isOpen) {
|
||||
const pendingDeepLink = localStorage.getItem('pendingScheduleDeepLink');
|
||||
if (pendingDeepLink) {
|
||||
localStorage.removeItem('pendingScheduleDeepLink');
|
||||
setSourceType('deeplink');
|
||||
handleDeepLinkChange(pendingDeepLink);
|
||||
}
|
||||
}
|
||||
}, [isOpen, handleDeepLinkChange]);
|
||||
|
||||
const resetForm = () => {
|
||||
setScheduleId('');
|
||||
setSourceType('file');
|
||||
setRecipeSourcePath('');
|
||||
setDeepLinkInput('');
|
||||
setParsedRecipe(null);
|
||||
setFrequency('daily');
|
||||
setSelectedDate(new Date().toISOString().split('T')[0]);
|
||||
setSelectedTime('09:00');
|
||||
@@ -195,10 +418,48 @@ export const CreateScheduleModal: React.FC<CreateScheduleModalProps> = ({
|
||||
setInternalValidationError('Schedule ID is required.');
|
||||
return;
|
||||
}
|
||||
if (!recipeSourcePath) {
|
||||
setInternalValidationError('Recipe source file is required.');
|
||||
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 {
|
||||
// Convert recipe to YAML and save to a temporary file
|
||||
const yamlContent = recipeToYaml(parsedRecipe);
|
||||
console.log('Generated YAML content:', yamlContent); // Debug log
|
||||
const tempFileName = `schedule-${scheduleId}-${Date.now()}.yaml`;
|
||||
const tempDir = window.electron.getConfig().GOOSE_WORKING_DIR || '.';
|
||||
const tempFilePath = `${tempDir}/${tempFileName}`;
|
||||
|
||||
// Write the YAML file
|
||||
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 (
|
||||
!derivedCronExpression ||
|
||||
derivedCronExpression.includes('Invalid') ||
|
||||
@@ -216,7 +477,7 @@ export const CreateScheduleModal: React.FC<CreateScheduleModalProps> = ({
|
||||
|
||||
const newSchedulePayload: NewSchedulePayload = {
|
||||
id: scheduleId.trim(),
|
||||
recipe_source: recipeSourcePath,
|
||||
recipe_source: finalRecipeSource,
|
||||
cron: derivedCronExpression,
|
||||
};
|
||||
|
||||
@@ -232,7 +493,7 @@ export const CreateScheduleModal: React.FC<CreateScheduleModalProps> = ({
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/20 backdrop-blur-sm z-40 flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-md bg-bgApp shadow-xl rounded-lg z-50 flex flex-col max-h-[90vh] overflow-hidden">
|
||||
<Card className="w-full max-w-md bg-bgApp shadow-xl rounded-lg z-50 flex flex-col max-h-[90vh] overflow-hidden">
|
||||
<div className="px-6 pt-6 pb-4 flex-shrink-0">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
Create New Schedule
|
||||
@@ -268,22 +529,79 @@ export const CreateScheduleModal: React.FC<CreateScheduleModalProps> = ({
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className={modalLabelClassName}>Recipe Source (YAML File):</label>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleBrowseFile}
|
||||
className="w-full justify-center"
|
||||
>
|
||||
Browse...
|
||||
</Button>
|
||||
{recipeSourcePath && (
|
||||
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400 italic">
|
||||
Selected: {recipeSourcePath}
|
||||
</p>
|
||||
)}
|
||||
<label className={modalLabelClassName}>Recipe Source:</label>
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSourceType('file')}
|
||||
className={`px-3 py-2 text-sm rounded-md border ${
|
||||
sourceType === 'file'
|
||||
? 'bg-blue-500 text-white border-blue-500'
|
||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600'
|
||||
}`}
|
||||
>
|
||||
YAML File
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSourceType('deeplink')}
|
||||
className={`px-3 py-2 text-sm rounded-md border ${
|
||||
sourceType === 'deeplink'
|
||||
? 'bg-blue-500 text-white border-blue-500'
|
||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600'
|
||||
}`}
|
||||
>
|
||||
Deep Link
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{sourceType === 'file' && (
|
||||
<div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleBrowseFile}
|
||||
className="w-full justify-center"
|
||||
>
|
||||
Browse for YAML file...
|
||||
</Button>
|
||||
{recipeSourcePath && (
|
||||
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400 italic">
|
||||
Selected: {recipeSourcePath}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{sourceType === 'deeplink' && (
|
||||
<div>
|
||||
<Input
|
||||
type="text"
|
||||
value={deepLinkInput}
|
||||
onChange={(e) => handleDeepLinkChange(e.target.value)}
|
||||
placeholder="Paste goose://bot or goose://recipe link here..."
|
||||
/>
|
||||
{parsedRecipe && (
|
||||
<div className="mt-2 p-2 bg-green-100 dark:bg-green-900/30 rounded-md border border-green-500/50">
|
||||
<p className="text-xs text-green-700 dark:text-green-300 font-medium">
|
||||
✓ Recipe parsed successfully
|
||||
</p>
|
||||
<p className="text-xs text-green-600 dark:text-green-400">
|
||||
Title: {parsedRecipe.title}
|
||||
</p>
|
||||
<p className="text-xs text-green-600 dark:text-green-400">
|
||||
Description: {parsedRecipe.description}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="frequency-modal" className={modalLabelClassName}>
|
||||
Frequency:
|
||||
|
||||
147
ui/desktop/src/components/schedule/ScheduleFromRecipeModal.tsx
Normal file
147
ui/desktop/src/components/schedule/ScheduleFromRecipeModal.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card } from '../ui/card';
|
||||
import { Button } from '../ui/button';
|
||||
import { Input } from '../ui/input';
|
||||
import { Recipe } from '../../recipe';
|
||||
import { generateDeepLink } from '../ui/DeepLinkModal';
|
||||
import Copy from '../icons/Copy';
|
||||
import { Check } from 'lucide-react';
|
||||
|
||||
interface ScheduleFromRecipeModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
recipe: Recipe;
|
||||
onCreateSchedule: (deepLink: string) => void;
|
||||
}
|
||||
|
||||
export const ScheduleFromRecipeModal: React.FC<ScheduleFromRecipeModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
recipe,
|
||||
onCreateSchedule,
|
||||
}) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [deepLink, setDeepLink] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && recipe) {
|
||||
// Convert Recipe to the format expected by generateDeepLink
|
||||
const recipeConfig = {
|
||||
id: recipe.title?.toLowerCase().replace(/[^a-z0-9-]/g, '-') || 'recipe',
|
||||
title: recipe.title,
|
||||
description: recipe.description,
|
||||
instructions: recipe.instructions,
|
||||
activities: recipe.activities || [],
|
||||
prompt: recipe.prompt,
|
||||
extensions: recipe.extensions,
|
||||
goosehints: recipe.goosehints,
|
||||
context: recipe.context,
|
||||
profile: recipe.profile,
|
||||
author: recipe.author,
|
||||
};
|
||||
const link = generateDeepLink(recipeConfig);
|
||||
setDeepLink(link);
|
||||
}
|
||||
}, [isOpen, recipe]);
|
||||
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard
|
||||
.writeText(deepLink)
|
||||
.then(() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Failed to copy the text:', err);
|
||||
});
|
||||
};
|
||||
|
||||
const handleCreateSchedule = () => {
|
||||
onCreateSchedule(deepLink);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setCopied(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/20 backdrop-blur-sm z-40 flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-md bg-bgApp shadow-xl rounded-lg z-50 flex flex-col">
|
||||
<div className="px-6 pt-6 pb-4">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
Create Schedule from Recipe
|
||||
</h2>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mt-2">
|
||||
Create a scheduled task using this recipe configuration.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="px-6 py-4 space-y-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Recipe Details:
|
||||
</h3>
|
||||
<div className="bg-gray-50 dark:bg-gray-800 p-3 rounded-md">
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{recipe.title}
|
||||
</p>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1">
|
||||
{recipe.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Recipe Deep Link:
|
||||
</label>
|
||||
<div className="flex items-center">
|
||||
<Input
|
||||
type="text"
|
||||
value={deepLink}
|
||||
readOnly
|
||||
className="flex-1 text-xs font-mono"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
className="ml-2 px-3 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 flex items-center"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="w-4 h-4" />
|
||||
) : (
|
||||
<Copy className="w-4 h-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
This link contains your recipe configuration and can be used to create a schedule.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-6 pb-6 flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleClose}
|
||||
className="flex-1"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleCreateSchedule}
|
||||
className="flex-1 bg-green-500 hover:bg-green-600 text-white"
|
||||
>
|
||||
Create Schedule
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -67,6 +67,14 @@ const SchedulesView: React.FC<SchedulesViewProps> = ({ onClose }) => {
|
||||
useEffect(() => {
|
||||
if (viewingScheduleId === null) {
|
||||
fetchSchedules();
|
||||
|
||||
// Check for pending deep link from recipe editor
|
||||
const pendingDeepLink = localStorage.getItem('pendingScheduleDeepLink');
|
||||
if (pendingDeepLink) {
|
||||
localStorage.removeItem('pendingScheduleDeepLink');
|
||||
setIsCreateModalOpen(true);
|
||||
// The CreateScheduleModal will handle the deep link
|
||||
}
|
||||
}
|
||||
}, [viewingScheduleId]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user