mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
Add recipe download and copy yaml (#6201)
This commit is contained in:
@@ -392,6 +392,7 @@ derive_utoipa!(Icon as IconSchema);
|
||||
super::routes::recipe::set_recipe_slash_command,
|
||||
super::routes::recipe::save_recipe,
|
||||
super::routes::recipe::parse_recipe,
|
||||
super::routes::recipe::recipe_to_yaml,
|
||||
super::routes::setup::start_openrouter_setup,
|
||||
super::routes::setup::start_tetrate_setup,
|
||||
super::routes::tunnel::start_tunnel,
|
||||
@@ -508,6 +509,8 @@ derive_utoipa!(Icon as IconSchema);
|
||||
super::routes::errors::ErrorResponse,
|
||||
super::routes::recipe::ParseRecipeRequest,
|
||||
super::routes::recipe::ParseRecipeResponse,
|
||||
super::routes::recipe::RecipeToYamlRequest,
|
||||
super::routes::recipe::RecipeToYamlResponse,
|
||||
goose::recipe::Recipe,
|
||||
goose::recipe::Author,
|
||||
goose::recipe::Settings,
|
||||
|
||||
@@ -137,6 +137,16 @@ pub struct SetSlashCommandRequest {
|
||||
slash_command: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct RecipeToYamlRequest {
|
||||
recipe: Recipe,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct RecipeToYamlResponse {
|
||||
yaml: String,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/recipes/create",
|
||||
@@ -520,6 +530,27 @@ async fn parse_recipe(
|
||||
Ok(Json(ParseRecipeResponse { recipe }))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/recipes/to-yaml",
|
||||
request_body = RecipeToYamlRequest,
|
||||
responses(
|
||||
(status = 200, description = "Recipe converted to YAML successfully", body = RecipeToYamlResponse),
|
||||
(status = 400, description = "Bad request - Failed to convert recipe to YAML", body = ErrorResponse),
|
||||
),
|
||||
tag = "Recipe Management"
|
||||
)]
|
||||
async fn recipe_to_yaml(
|
||||
Json(request): Json<RecipeToYamlRequest>,
|
||||
) -> Result<Json<RecipeToYamlResponse>, ErrorResponse> {
|
||||
let yaml = request.recipe.to_yaml().map_err(|e| ErrorResponse {
|
||||
message: format!("Failed to convert recipe to YAML: {}", e),
|
||||
status: StatusCode::BAD_REQUEST,
|
||||
})?;
|
||||
|
||||
Ok(Json(RecipeToYamlResponse { yaml }))
|
||||
}
|
||||
|
||||
pub fn routes(state: Arc<AppState>) -> Router {
|
||||
Router::new()
|
||||
.route("/recipes/create", post(create_recipe))
|
||||
@@ -532,6 +563,7 @@ pub fn routes(state: Arc<AppState>) -> Router {
|
||||
.route("/recipes/slash-command", post(set_recipe_slash_command))
|
||||
.route("/recipes/save", post(save_recipe))
|
||||
.route("/recipes/parse", post(parse_recipe))
|
||||
.route("/recipes/to-yaml", post(recipe_to_yaml))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
|
||||
@@ -1558,6 +1558,46 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/recipes/to-yaml": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Recipe Management"
|
||||
],
|
||||
"operationId": "recipe_to_yaml",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/RecipeToYamlRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Recipe converted to YAML successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/RecipeToYamlResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request - Failed to convert recipe to YAML",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/reply": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@@ -4762,6 +4802,28 @@
|
||||
"user_prompt"
|
||||
]
|
||||
},
|
||||
"RecipeToYamlRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"recipe"
|
||||
],
|
||||
"properties": {
|
||||
"recipe": {
|
||||
"$ref": "#/components/schemas/Recipe"
|
||||
}
|
||||
}
|
||||
},
|
||||
"RecipeToYamlResponse": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"yaml"
|
||||
],
|
||||
"properties": {
|
||||
"yaml": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"RedactedThinkingContent": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import type { Client, Options as Options2, TDataShape } from './client';
|
||||
import { client } from './client.gen';
|
||||
import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, CallToolData, CallToolErrors, CallToolResponses, CheckProviderData, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DetectProviderData, DetectProviderErrors, DetectProviderResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, EditMessageData, EditMessageErrors, EditMessageResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetPricingData, GetPricingResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeResponses, SendTelemetryEventData, SendTelemetryEventResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StartTunnelData, StartTunnelErrors, StartTunnelResponses, StatusData, StatusResponses, StopTunnelData, StopTunnelErrors, StopTunnelResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen';
|
||||
import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, CallToolData, CallToolErrors, CallToolResponses, CheckProviderData, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DetectProviderData, DetectProviderErrors, DetectProviderResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, EditMessageData, EditMessageErrors, EditMessageResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetPricingData, GetPricingResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceResponses, RecipeToYamlData, RecipeToYamlErrors, RecipeToYamlResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeResponses, SendTelemetryEventData, SendTelemetryEventResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StartTunnelData, StartTunnelErrors, StartTunnelResponses, StatusData, StatusResponses, StopTunnelData, StopTunnelErrors, StopTunnelResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen';
|
||||
|
||||
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
|
||||
/**
|
||||
@@ -315,6 +315,15 @@ export const setRecipeSlashCommand = <ThrowOnError extends boolean = false>(opti
|
||||
}
|
||||
});
|
||||
|
||||
export const recipeToYaml = <ThrowOnError extends boolean = false>(options: Options<RecipeToYamlData, ThrowOnError>) => (options.client ?? client).post<RecipeToYamlResponses, RecipeToYamlErrors, ThrowOnError>({
|
||||
url: '/recipes/to-yaml',
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers
|
||||
}
|
||||
});
|
||||
|
||||
export const reply = <ThrowOnError extends boolean = false>(options: Options<ReplyData, ThrowOnError>) => (options.client ?? client).sse.post<ReplyResponses, ReplyErrors, ThrowOnError>({
|
||||
url: '/reply',
|
||||
...options,
|
||||
|
||||
@@ -732,6 +732,14 @@ export type RecipeParameterInputType = 'string' | 'number' | 'boolean' | 'date'
|
||||
|
||||
export type RecipeParameterRequirement = 'required' | 'optional' | 'user_prompt';
|
||||
|
||||
export type RecipeToYamlRequest = {
|
||||
recipe: Recipe;
|
||||
};
|
||||
|
||||
export type RecipeToYamlResponse = {
|
||||
yaml: string;
|
||||
};
|
||||
|
||||
export type RedactedThinkingContent = {
|
||||
data: string;
|
||||
};
|
||||
@@ -2325,6 +2333,31 @@ export type SetRecipeSlashCommandResponses = {
|
||||
200: unknown;
|
||||
};
|
||||
|
||||
export type RecipeToYamlData = {
|
||||
body: RecipeToYamlRequest;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/recipes/to-yaml';
|
||||
};
|
||||
|
||||
export type RecipeToYamlErrors = {
|
||||
/**
|
||||
* Bad request - Failed to convert recipe to YAML
|
||||
*/
|
||||
400: ErrorResponse;
|
||||
};
|
||||
|
||||
export type RecipeToYamlError = RecipeToYamlErrors[keyof RecipeToYamlErrors];
|
||||
|
||||
export type RecipeToYamlResponses = {
|
||||
/**
|
||||
* Recipe converted to YAML successfully
|
||||
*/
|
||||
200: RecipeToYamlResponse;
|
||||
};
|
||||
|
||||
export type RecipeToYamlResponse2 = RecipeToYamlResponses[keyof RecipeToYamlResponses];
|
||||
|
||||
export type ReplyData = {
|
||||
body: ChatRequest;
|
||||
path?: never;
|
||||
|
||||
@@ -12,7 +12,11 @@ export type McpMethodParams = {
|
||||
export type McpMethodResponse = {
|
||||
'ui/open-link': { status: string; message: string };
|
||||
'ui/message': { status: string; message: string };
|
||||
'tools/call': { content: unknown[]; isError: boolean; structuredContent?: Record<string, unknown> };
|
||||
'tools/call': {
|
||||
content: unknown[];
|
||||
isError: boolean;
|
||||
structuredContent?: Record<string, unknown>;
|
||||
};
|
||||
'resources/read': { contents: unknown[] };
|
||||
'notifications/message': Record<string, never>;
|
||||
ping: Record<string, never>;
|
||||
|
||||
@@ -275,7 +275,7 @@ const getToolName = (toolCallName: string): string => {
|
||||
if (lastIndex === -1) return toolCallName;
|
||||
|
||||
return toolCallName.substring(lastIndex + 2);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to extract extension name for tooltip
|
||||
const getExtensionTooltip = (toolCallName: string): string | null => {
|
||||
|
||||
@@ -11,13 +11,16 @@ import {
|
||||
Clock,
|
||||
Terminal,
|
||||
ExternalLink,
|
||||
Share2,
|
||||
Copy,
|
||||
Download,
|
||||
} from 'lucide-react';
|
||||
import { ScrollArea } from '../ui/scroll-area';
|
||||
import { Card } from '../ui/card';
|
||||
import { Button } from '../ui/button';
|
||||
import { Skeleton } from '../ui/skeleton';
|
||||
import { MainPanelLayout } from '../Layout/MainPanelLayout';
|
||||
import { toastSuccess } from '../../toasts';
|
||||
import { toastSuccess, toastError } from '../../toasts';
|
||||
import { useEscapeKey } from '../../hooks/useEscapeKey';
|
||||
import {
|
||||
deleteRecipe,
|
||||
@@ -25,6 +28,7 @@ import {
|
||||
startAgent,
|
||||
scheduleRecipe,
|
||||
setRecipeSlashCommand,
|
||||
recipeToYaml,
|
||||
} from '../../api';
|
||||
import ImportRecipeForm, { ImportRecipeButton } from './ImportRecipeForm';
|
||||
import CreateEditRecipeModal from './CreateEditRecipeModal';
|
||||
@@ -38,10 +42,19 @@ import {
|
||||
trackRecipeDeleted,
|
||||
trackRecipeStarted,
|
||||
trackRecipeDeeplinkCopied,
|
||||
trackRecipeYamlCopied,
|
||||
trackRecipeExportedToFile,
|
||||
trackRecipeScheduled,
|
||||
trackRecipeSlashCommandSet,
|
||||
getErrorType,
|
||||
} from '../../utils/analytics';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuSeparator,
|
||||
} from '../ui/dropdown-menu';
|
||||
|
||||
export default function RecipesView() {
|
||||
const setView = useNavigation();
|
||||
@@ -217,13 +230,85 @@ export default function RecipesView() {
|
||||
} catch (error) {
|
||||
console.error('Failed to copy deeplink:', error);
|
||||
trackRecipeDeeplinkCopied(false, getErrorType(error));
|
||||
toastSuccess({
|
||||
toastError({
|
||||
title: 'Copy failed',
|
||||
msg: 'Failed to copy deeplink to clipboard',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyYaml = async (recipeManifest: RecipeManifest) => {
|
||||
try {
|
||||
const response = await recipeToYaml({
|
||||
body: { recipe: recipeManifest.recipe },
|
||||
throwOnError: true,
|
||||
});
|
||||
|
||||
if (!response.data?.yaml) {
|
||||
throw new Error('No YAML data returned from API');
|
||||
}
|
||||
|
||||
await navigator.clipboard.writeText(response.data.yaml);
|
||||
trackRecipeYamlCopied(true);
|
||||
toastSuccess({
|
||||
title: 'YAML copied',
|
||||
msg: 'Recipe YAML has been copied to clipboard',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to copy YAML:', error);
|
||||
trackRecipeYamlCopied(false, getErrorType(error));
|
||||
toastError({
|
||||
title: 'Copy failed',
|
||||
msg: 'Failed to copy recipe YAML to clipboard',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleExportFile = async (recipeManifest: RecipeManifest) => {
|
||||
try {
|
||||
const response = await recipeToYaml({
|
||||
body: { recipe: recipeManifest.recipe },
|
||||
throwOnError: true,
|
||||
});
|
||||
|
||||
if (!response.data?.yaml) {
|
||||
throw new Error('No YAML data returned from API');
|
||||
}
|
||||
|
||||
const sanitizedTitle = (recipeManifest.recipe.title || 'recipe')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
|
||||
const filename = `${sanitizedTitle}.yaml`;
|
||||
|
||||
const result = await window.electron.showSaveDialog({
|
||||
title: 'Export Recipe',
|
||||
defaultPath: filename,
|
||||
filters: [
|
||||
{ name: 'YAML Files', extensions: ['yaml', 'yml'] },
|
||||
{ name: 'All Files', extensions: ['*'] },
|
||||
],
|
||||
});
|
||||
|
||||
if (!result.canceled && result.filePath) {
|
||||
await window.electron.writeFile(result.filePath, response.data.yaml);
|
||||
trackRecipeExportedToFile(true);
|
||||
toastSuccess({
|
||||
title: 'Recipe exported',
|
||||
msg: `Recipe saved to ${result.filePath}`,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to export recipe:', error);
|
||||
trackRecipeExportedToFile(false, getErrorType(error));
|
||||
toastError({
|
||||
title: 'Export failed',
|
||||
msg: 'Failed to export recipe to file',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenScheduleDialog = (recipeManifest: RecipeManifest) => {
|
||||
setScheduleRecipeManifest(recipeManifest);
|
||||
setScheduleCron(recipeManifest.schedule_cron || '0 0 14 * * *');
|
||||
@@ -450,18 +535,34 @@ export default function RecipesView() {
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleCopyDeeplink(recipeManifestResponse);
|
||||
}}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
title="Copy deeplink"
|
||||
>
|
||||
<Link className="w-4 h-4" />
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
title="Share recipe"
|
||||
>
|
||||
<Share2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" onClick={(e) => e.stopPropagation()}>
|
||||
<DropdownMenuItem onClick={() => handleCopyDeeplink(recipeManifestResponse)}>
|
||||
<Link className="w-4 h-4" />
|
||||
Copy Deeplink
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleCopyYaml(recipeManifestResponse)}>
|
||||
<Copy className="w-4 h-4" />
|
||||
Copy YAML
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => handleExportFile(recipeManifestResponse)}>
|
||||
<Download className="w-4 h-4" />
|
||||
Export to File
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -38,7 +38,8 @@ export default function ExtensionsSection({
|
||||
onModalClose,
|
||||
searchTerm = '',
|
||||
}: ExtensionSectionProps) {
|
||||
const { getExtensions, addExtension, removeExtension, extensionsList, extensionWarnings } = useConfig();
|
||||
const { getExtensions, addExtension, removeExtension, extensionsList, extensionWarnings } =
|
||||
useConfig();
|
||||
const [selectedExtension, setSelectedExtension] = useState<FixedExtensionEntry | null>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||
|
||||
@@ -1766,11 +1766,14 @@ ipcMain.handle('list-files', async (_event, dirPath, extension) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Handle message box dialogs
|
||||
ipcMain.handle('show-message-box', async (_event, options) => {
|
||||
return dialog.showMessageBox(options);
|
||||
});
|
||||
|
||||
ipcMain.handle('show-save-dialog', async (_event, options) => {
|
||||
return dialog.showSaveDialog(options);
|
||||
});
|
||||
|
||||
ipcMain.handle('get-allowed-extensions', async () => {
|
||||
return await getAllowList();
|
||||
});
|
||||
|
||||
@@ -20,6 +20,21 @@ interface MessageBoxResponse {
|
||||
checkboxChecked?: boolean;
|
||||
}
|
||||
|
||||
interface SaveDialogOptions {
|
||||
title?: string;
|
||||
defaultPath?: string;
|
||||
buttonLabel?: string;
|
||||
filters?: Array<{ name: string; extensions: string[] }>;
|
||||
message?: string;
|
||||
nameFieldLabel?: string;
|
||||
showsTagField?: boolean;
|
||||
}
|
||||
|
||||
interface SaveDialogResponse {
|
||||
canceled: boolean;
|
||||
filePath?: string;
|
||||
}
|
||||
|
||||
interface FileResponse {
|
||||
file: string;
|
||||
filePath: string;
|
||||
@@ -58,6 +73,7 @@ type ElectronAPI = {
|
||||
logInfo: (txt: string) => void;
|
||||
showNotification: (data: NotificationData) => void;
|
||||
showMessageBox: (options: MessageBoxOptions) => Promise<MessageBoxResponse>;
|
||||
showSaveDialog: (options: SaveDialogOptions) => Promise<SaveDialogResponse>;
|
||||
openInChrome: (url: string) => void;
|
||||
fetchMetadata: (url: string) => Promise<string>;
|
||||
reloadApp: () => void;
|
||||
@@ -158,6 +174,7 @@ const electronAPI: ElectronAPI = {
|
||||
logInfo: (txt: string) => ipcRenderer.send('logInfo', txt),
|
||||
showNotification: (data: NotificationData) => ipcRenderer.send('notify', data),
|
||||
showMessageBox: (options: MessageBoxOptions) => ipcRenderer.invoke('show-message-box', options),
|
||||
showSaveDialog: (options: SaveDialogOptions) => ipcRenderer.invoke('show-save-dialog', options),
|
||||
openInChrome: (url: string) => ipcRenderer.send('open-in-chrome', url),
|
||||
fetchMetadata: (url: string) => ipcRenderer.invoke('fetch-metadata', url),
|
||||
reloadApp: () => ipcRenderer.send('reload-app'),
|
||||
|
||||
@@ -115,6 +115,8 @@ export type AnalyticsEvent =
|
||||
properties: { success: boolean; error_details?: string; in_new_window?: boolean };
|
||||
}
|
||||
| { name: 'recipe_deeplink_copied'; properties: { success: boolean; error_details?: string } }
|
||||
| { name: 'recipe_yaml_copied'; properties: { success: boolean; error_details?: string } }
|
||||
| { name: 'recipe_exported_to_file'; properties: { success: boolean; error_details?: string } }
|
||||
| {
|
||||
name: 'recipe_scheduled';
|
||||
properties: { success: boolean; error_details?: string; action: 'add' | 'edit' | 'remove' };
|
||||
@@ -536,6 +538,20 @@ export function trackRecipeDeeplinkCopied(success: boolean, errorDetails?: strin
|
||||
});
|
||||
}
|
||||
|
||||
export function trackRecipeYamlCopied(success: boolean, errorDetails?: string): void {
|
||||
trackEvent({
|
||||
name: 'recipe_yaml_copied',
|
||||
properties: { success, error_details: errorDetails },
|
||||
});
|
||||
}
|
||||
|
||||
export function trackRecipeExportedToFile(success: boolean, errorDetails?: string): void {
|
||||
trackEvent({
|
||||
name: 'recipe_exported_to_file',
|
||||
properties: { success, error_details: errorDetails },
|
||||
});
|
||||
}
|
||||
|
||||
export function trackRecipeScheduled(
|
||||
success: boolean,
|
||||
action: 'add' | 'edit' | 'remove',
|
||||
|
||||
Reference in New Issue
Block a user