mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-18 14:59:42 +02:00
Export multiple workspaces
This commit is contained in:
@@ -42,7 +42,7 @@ use window_ext::TrafficLightWindowExt;
|
|||||||
use crate::analytics::{AnalyticsAction, AnalyticsResource};
|
use crate::analytics::{AnalyticsAction, AnalyticsResource};
|
||||||
use crate::grpc::metadata_to_map;
|
use crate::grpc::metadata_to_map;
|
||||||
use crate::http::send_http_request;
|
use crate::http::send_http_request;
|
||||||
use crate::models::{cancel_pending_grpc_connections, cancel_pending_responses, CookieJar, create_http_response, delete_all_grpc_connections, delete_all_http_responses, delete_cookie_jar, delete_environment, delete_folder, delete_grpc_connection, delete_grpc_request, delete_http_request, delete_http_response, delete_workspace, duplicate_grpc_request, duplicate_http_request, Environment, EnvironmentVariable, Folder, get_cookie_jar, get_environment, get_folder, get_grpc_connection, get_grpc_request, get_http_request, get_http_response, get_key_value_raw, get_or_create_settings, get_workspace, get_workspace_export_resources, GrpcConnection, GrpcEvent, GrpcEventType, GrpcRequest, HttpRequest, HttpRequestHeader, HttpResponse, KeyValue, list_cookie_jars, list_environments, list_folders, list_grpc_connections, list_grpc_events, list_grpc_requests, list_requests, list_responses, list_workspaces, set_key_value_raw, Settings, update_response_if_id, update_settings, upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection, upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_workspace, Workspace, WorkspaceExportResources};
|
use crate::models::{cancel_pending_grpc_connections, cancel_pending_responses, CookieJar, create_http_response, delete_all_grpc_connections, delete_all_http_responses, delete_cookie_jar, delete_environment, delete_folder, delete_grpc_connection, delete_grpc_request, delete_http_request, delete_http_response, delete_workspace, duplicate_grpc_request, duplicate_http_request, Environment, EnvironmentVariable, Folder, get_cookie_jar, get_environment, get_folder, get_grpc_connection, get_grpc_request, get_http_request, get_http_response, get_key_value_raw, get_or_create_settings, get_workspace, get_workspace_export_resources, GrpcConnection, GrpcEvent, GrpcEventType, GrpcRequest, HttpRequest, HttpRequestHeader, HttpResponse, KeyValue, list_cookie_jars, list_environments, list_folders, list_grpc_connections, list_grpc_events, list_grpc_requests, list_http_requests, list_responses, list_workspaces, set_key_value_raw, Settings, update_response_if_id, update_settings, upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection, upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_workspace, Workspace, WorkspaceExportResources};
|
||||||
use crate::plugin::ImportResult;
|
use crate::plugin::ImportResult;
|
||||||
use crate::updates::{update_mode_from_str, UpdateMode, YaakUpdater};
|
use crate::updates::{update_mode_from_str, UpdateMode, YaakUpdater};
|
||||||
|
|
||||||
@@ -783,9 +783,9 @@ async fn cmd_import_data(
|
|||||||
async fn cmd_export_data(
|
async fn cmd_export_data(
|
||||||
app_handle: AppHandle,
|
app_handle: AppHandle,
|
||||||
export_path: &str,
|
export_path: &str,
|
||||||
workspace_id: &str,
|
workspace_ids: Vec<&str>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let export_data = get_workspace_export_resources(&app_handle, workspace_id).await;
|
let export_data = get_workspace_export_resources(&app_handle, workspace_ids).await;
|
||||||
let f = File::options()
|
let f = File::options()
|
||||||
.create(true)
|
.create(true)
|
||||||
.truncate(true)
|
.truncate(true)
|
||||||
@@ -1194,7 +1194,7 @@ async fn cmd_list_grpc_requests(workspace_id: &str, w: Window) -> Result<Vec<Grp
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn cmd_list_http_requests(workspace_id: &str, w: Window) -> Result<Vec<HttpRequest>, String> {
|
async fn cmd_list_http_requests(workspace_id: &str, w: Window) -> Result<Vec<HttpRequest>, String> {
|
||||||
let requests = list_requests(&w, workspace_id)
|
let requests = list_http_requests(&w, workspace_id)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to find requests");
|
.expect("Failed to find requests");
|
||||||
// .map_err(|e| e.to_string())
|
// .map_err(|e| e.to_string())
|
||||||
|
|||||||
@@ -1115,7 +1115,7 @@ pub async fn upsert_http_request(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_requests(
|
pub async fn list_http_requests(
|
||||||
mgr: &impl Manager<Wry>,
|
mgr: &impl Manager<Wry>,
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
) -> Result<Vec<HttpRequest>, sqlx::Error> {
|
) -> Result<Vec<HttpRequest>, sqlx::Error> {
|
||||||
@@ -1524,49 +1524,68 @@ pub fn generate_id(prefix: Option<&str>) -> String {
|
|||||||
#[derive(Default, Debug, Deserialize, Serialize)]
|
#[derive(Default, Debug, Deserialize, Serialize)]
|
||||||
#[serde(default, rename_all = "camelCase")]
|
#[serde(default, rename_all = "camelCase")]
|
||||||
pub struct WorkspaceExport {
|
pub struct WorkspaceExport {
|
||||||
pub yaak_version: String,
|
pub yaak_version: String,
|
||||||
pub yaak_schema: i64,
|
pub yaak_schema: i64,
|
||||||
pub timestamp: NaiveDateTime,
|
pub timestamp: NaiveDateTime,
|
||||||
pub resources: WorkspaceExportResources,
|
pub resources: WorkspaceExportResources,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Deserialize, Serialize)]
|
#[derive(Default, Debug, Deserialize, Serialize)]
|
||||||
#[serde(default, rename_all = "camelCase")]
|
#[serde(default, rename_all = "camelCase")]
|
||||||
pub struct WorkspaceExportResources {
|
pub struct WorkspaceExportResources {
|
||||||
pub workspaces: Vec<Workspace>,
|
pub workspaces: Vec<Workspace>,
|
||||||
pub environments: Vec<Environment>,
|
pub environments: Vec<Environment>,
|
||||||
pub folders: Vec<Folder>,
|
pub folders: Vec<Folder>,
|
||||||
pub http_requests: Vec<HttpRequest>,
|
pub http_requests: Vec<HttpRequest>,
|
||||||
pub grpc_requests: Vec<GrpcRequest>,
|
pub grpc_requests: Vec<GrpcRequest>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_workspace_export_resources(
|
pub async fn get_workspace_export_resources(
|
||||||
app_handle: &AppHandle,
|
app_handle: &AppHandle,
|
||||||
workspace_id: &str,
|
workspace_ids: Vec<&str>,
|
||||||
) -> WorkspaceExport {
|
) -> WorkspaceExport {
|
||||||
let workspace = get_workspace(app_handle, workspace_id)
|
let mut data = WorkspaceExport {
|
||||||
.await
|
|
||||||
.expect("Failed to get workspace");
|
|
||||||
return WorkspaceExport {
|
|
||||||
yaak_version: app_handle.package_info().version.clone().to_string(),
|
yaak_version: app_handle.package_info().version.clone().to_string(),
|
||||||
yaak_schema: 2,
|
yaak_schema: 2,
|
||||||
timestamp: chrono::Utc::now().naive_utc(),
|
timestamp: chrono::Utc::now().naive_utc(),
|
||||||
resources: WorkspaceExportResources {
|
resources: WorkspaceExportResources {
|
||||||
workspaces: vec![workspace],
|
workspaces: Vec::new(),
|
||||||
environments: list_environments(app_handle, workspace_id)
|
environments: Vec::new(),
|
||||||
.await
|
folders: Vec::new(),
|
||||||
.expect("Failed to get environments"),
|
http_requests: Vec::new(),
|
||||||
folders: list_folders(app_handle, workspace_id)
|
grpc_requests: Vec::new(),
|
||||||
.await
|
|
||||||
.expect("Failed to get folders"),
|
|
||||||
http_requests: list_requests(app_handle, workspace_id)
|
|
||||||
.await
|
|
||||||
.expect("Failed to get requests"),
|
|
||||||
grpc_requests: list_grpc_requests(app_handle, workspace_id)
|
|
||||||
.await
|
|
||||||
.expect("Failed to get grpc requests"),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
for workspace_id in workspace_ids {
|
||||||
|
data.resources.workspaces.push(
|
||||||
|
get_workspace(app_handle, workspace_id)
|
||||||
|
.await
|
||||||
|
.expect("Failed to get workspace"),
|
||||||
|
);
|
||||||
|
data.resources.environments.append(
|
||||||
|
&mut list_environments(app_handle, workspace_id)
|
||||||
|
.await
|
||||||
|
.expect("Failed to get environments"),
|
||||||
|
);
|
||||||
|
data.resources.folders.append(
|
||||||
|
&mut list_folders(app_handle, workspace_id)
|
||||||
|
.await
|
||||||
|
.expect("Failed to get folders"),
|
||||||
|
);
|
||||||
|
data.resources.http_requests.append(
|
||||||
|
&mut list_http_requests(app_handle, workspace_id)
|
||||||
|
.await
|
||||||
|
.expect("Failed to get http requests"),
|
||||||
|
);
|
||||||
|
data.resources.grpc_requests.append(
|
||||||
|
&mut list_grpc_requests(app_handle, workspace_id)
|
||||||
|
.await
|
||||||
|
.expect("Failed to get grpc requests"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn emit_upserted_model<S: Serialize + Clone>(mgr: &impl Manager<Wry>, model: S) -> S {
|
fn emit_upserted_model<S: Serialize + Clone>(mgr: &impl Manager<Wry>, model: S) -> S {
|
||||||
|
|||||||
107
src-web/components/ExportDataDialog.tsx
Normal file
107
src-web/components/ExportDataDialog.tsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { invoke } from '@tauri-apps/api';
|
||||||
|
import { save } from '@tauri-apps/api/dialog';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import slugify from 'slugify';
|
||||||
|
import type { Workspace } from '../lib/models';
|
||||||
|
import { count } from '../lib/pluralize';
|
||||||
|
import { Button } from './core/Button';
|
||||||
|
import { Checkbox } from './core/Checkbox';
|
||||||
|
import { HStack, VStack } from './core/Stacks';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onHide: () => void;
|
||||||
|
activeWorkspace: Workspace;
|
||||||
|
workspaces: Workspace[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ExportDataDialog({ onHide, activeWorkspace, workspaces: allWorkspaces }: Props) {
|
||||||
|
const [selectedWorkspaces, setSelectedWorkspaces] = useState<Record<string, boolean>>({
|
||||||
|
[activeWorkspace.id]: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const workspaces = [activeWorkspace, ...allWorkspaces.filter((w) => w.id !== activeWorkspace.id)];
|
||||||
|
|
||||||
|
const handleToggleAll = () => {
|
||||||
|
setSelectedWorkspaces(
|
||||||
|
allSelected ? {} : workspaces.reduce((acc, w) => ({ ...acc, [w.id]: true }), {}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExport = async () => {
|
||||||
|
const ids = Object.keys(selectedWorkspaces).filter((k) => selectedWorkspaces[k]);
|
||||||
|
const workspace = ids.length === 1 ? workspaces.find((w) => w.id === ids[0]) : undefined;
|
||||||
|
const slug = workspace ? slugify(workspace.name, { lower: true }) : 'workspaces';
|
||||||
|
const exportPath = await save({
|
||||||
|
title: 'Export Data',
|
||||||
|
defaultPath: `yaak.${slug}.json`,
|
||||||
|
});
|
||||||
|
if (exportPath == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await invoke('cmd_export_data', { workspaceIds: ids, exportPath });
|
||||||
|
onHide();
|
||||||
|
};
|
||||||
|
|
||||||
|
const allSelected = workspaces.every((w) => selectedWorkspaces[w.id]);
|
||||||
|
const numSelected = Object.values(selectedWorkspaces).filter(Boolean).length;
|
||||||
|
const noneSelected = numSelected === 0;
|
||||||
|
return (
|
||||||
|
<VStack space={3} className="w-full mb-3 px-4">
|
||||||
|
<table className="w-full mb-auto min-w-full max-w-full divide-y">
|
||||||
|
<thead>
|
||||||
|
<tr onClick={handleToggleAll}>
|
||||||
|
<th className="w-6 min-w-0 py-2 text-left pl-1">
|
||||||
|
<Checkbox
|
||||||
|
checked={allSelected}
|
||||||
|
indeterminate={!allSelected && !noneSelected}
|
||||||
|
hideLabel
|
||||||
|
title="All workspaces"
|
||||||
|
onChange={handleToggleAll}
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
<th className="py-2 text-left pl-4">Workspace</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y">
|
||||||
|
{workspaces.map((w) => (
|
||||||
|
<tr
|
||||||
|
key={w.id}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedWorkspaces((prev) => ({ ...prev, [w.id]: !prev[w.id] }));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<td className="min-w-0 py-1 pl-1">
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedWorkspaces[w.id] ?? false}
|
||||||
|
title={w.name}
|
||||||
|
hideLabel
|
||||||
|
onChange={() => {
|
||||||
|
setSelectedWorkspaces((prev) => ({ ...prev, [w.id]: !prev[w.id] }));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className="py-1 pl-4 text-gray-700 whitespace-nowrap overflow-x-auto hide-scrollbars">
|
||||||
|
{w.name} {w.id === activeWorkspace.id ? '(current workspace)' : ''}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<HStack space={2} justifyContent="end">
|
||||||
|
<Button className="focus" color="gray" onClick={onHide}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="focus"
|
||||||
|
color="primary"
|
||||||
|
disabled={noneSelected}
|
||||||
|
onClick={handleExport}
|
||||||
|
>
|
||||||
|
Export {count('Workspace', numSelected, { omitSingle: true, noneWord: 'Nothing' })}
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -60,17 +60,27 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
|||||||
size === 'sm' && 'h-sm px-2.5 text-sm',
|
size === 'sm' && 'h-sm px-2.5 text-sm',
|
||||||
size === 'xs' && 'h-xs px-2 text-sm',
|
size === 'xs' && 'h-xs px-2 text-sm',
|
||||||
// Solids
|
// Solids
|
||||||
variant === 'solid' && color === 'custom' && 'ring-blue-400',
|
variant === 'solid' &&
|
||||||
|
color === 'custom' &&
|
||||||
|
'ring-blue-400 enabled:hocus:bg-highlightSecondary',
|
||||||
variant === 'solid' &&
|
variant === 'solid' &&
|
||||||
color === 'default' &&
|
color === 'default' &&
|
||||||
'text-gray-700 enabled:hocus:bg-gray-700/10 enabled:hocus:text-gray-800 ring-blue-400',
|
'text-gray-700 enabled:hocus:bg-gray-700/10 enabled:hocus:text-gray-800 ring-blue-400',
|
||||||
variant === 'solid' &&
|
variant === 'solid' &&
|
||||||
color === 'gray' &&
|
color === 'gray' &&
|
||||||
'text-gray-800 bg-highlight enabled:hocus:text-gray-1000 ring-blue-400',
|
'text-gray-800 bg-gray-200/70 enabled:hocus:bg-gray-200 ring-blue-400',
|
||||||
variant === 'solid' && color === 'primary' && 'bg-blue-400 text-white ring-blue-700',
|
variant === 'solid' &&
|
||||||
variant === 'solid' && color === 'secondary' && 'bg-violet-400 text-white ring-violet-700',
|
color === 'primary' &&
|
||||||
variant === 'solid' && color === 'warning' && 'bg-orange-400 text-white ring-orange-700',
|
'bg-blue-400 text-white ring-blue-700 enabled:hocus:bg-blue-500',
|
||||||
variant === 'solid' && color === 'danger' && 'bg-red-400 text-white ring-red-700',
|
variant === 'solid' &&
|
||||||
|
color === 'secondary' &&
|
||||||
|
'bg-violet-400 text-white ring-violet-700 enabled:hocus:bg-violet-500',
|
||||||
|
variant === 'solid' &&
|
||||||
|
color === 'warning' &&
|
||||||
|
'bg-orange-400 text-white ring-orange-700 enabled:hocus:bg-orange-500',
|
||||||
|
variant === 'solid' &&
|
||||||
|
color === 'danger' &&
|
||||||
|
'bg-red-400 text-white ring-red-700 enabled:hocus:bg-red-500',
|
||||||
// Borders
|
// Borders
|
||||||
variant === 'border' && 'border',
|
variant === 'border' && 'border',
|
||||||
variant === 'border' &&
|
variant === 'border' &&
|
||||||
|
|||||||
@@ -8,10 +8,21 @@ interface Props {
|
|||||||
onChange: (checked: boolean) => void;
|
onChange: (checked: boolean) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
inputWrapperClassName?: string;
|
||||||
|
indeterminate?: boolean;
|
||||||
hideLabel?: boolean;
|
hideLabel?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Checkbox({ checked, onChange, className, disabled, title, hideLabel }: Props) {
|
export function Checkbox({
|
||||||
|
checked,
|
||||||
|
indeterminate,
|
||||||
|
onChange,
|
||||||
|
className,
|
||||||
|
inputWrapperClassName,
|
||||||
|
disabled,
|
||||||
|
title,
|
||||||
|
hideLabel,
|
||||||
|
}: Props) {
|
||||||
return (
|
return (
|
||||||
<HStack
|
<HStack
|
||||||
as="label"
|
as="label"
|
||||||
@@ -19,16 +30,19 @@ export function Checkbox({ checked, onChange, className, disabled, title, hideLa
|
|||||||
alignItems="center"
|
alignItems="center"
|
||||||
className={classNames(className, 'text-gray-900 text-sm', disabled && 'opacity-disabled')}
|
className={classNames(className, 'text-gray-900 text-sm', disabled && 'opacity-disabled')}
|
||||||
>
|
>
|
||||||
<div className="relative flex">
|
<div className={classNames(inputWrapperClassName, 'relative flex')}>
|
||||||
<input
|
<input
|
||||||
aria-hidden
|
aria-hidden
|
||||||
className="appearance-none w-4 h-4 flex-shrink-0 border border-gray-200 rounded focus:border-focus outline-none ring-0"
|
className={classNames(
|
||||||
|
'opacity-50 appearance-none w-4 h-4 flex-shrink-0 border border-[currentColor]',
|
||||||
|
'rounded focus:border-focus focus:opacity-100 outline-none ring-0',
|
||||||
|
)}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={() => onChange(!checked)}
|
onChange={() => onChange(!checked)}
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
<Icon size="sm" icon={checked ? 'check' : 'empty'} />
|
<Icon size="sm" icon={indeterminate ? 'minus' : checked ? 'check' : 'empty'} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/*<button*/}
|
{/*<button*/}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const icons = {
|
|||||||
arrowUpFromDot: lucide.ArrowUpFromDotIcon,
|
arrowUpFromDot: lucide.ArrowUpFromDotIcon,
|
||||||
box: lucide.BoxIcon,
|
box: lucide.BoxIcon,
|
||||||
cake: lucide.CakeIcon,
|
cake: lucide.CakeIcon,
|
||||||
|
minus: lucide.MinusIcon,
|
||||||
chat: lucide.MessageSquare,
|
chat: lucide.MessageSquare,
|
||||||
check: lucide.CheckIcon,
|
check: lucide.CheckIcon,
|
||||||
chevronDown: lucide.ChevronDownIcon,
|
chevronDown: lucide.ChevronDownIcon,
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ export function Prompt({
|
|||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
hideLabel
|
hideLabel
|
||||||
require={require}
|
|
||||||
autoSelect
|
autoSelect
|
||||||
|
require={require}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
label={label}
|
label={label}
|
||||||
name={name}
|
name={name}
|
||||||
|
|||||||
@@ -1,31 +1,36 @@
|
|||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
import { invoke } from '@tauri-apps/api';
|
import { useDialog } from '../components/DialogContext';
|
||||||
import { save } from '@tauri-apps/api/dialog';
|
import { ExportDataDialog } from '../components/ExportDataDialog';
|
||||||
import slugify from 'slugify';
|
|
||||||
import { useActiveWorkspace } from './useActiveWorkspace';
|
import { useActiveWorkspace } from './useActiveWorkspace';
|
||||||
import { useAlert } from './useAlert';
|
import { useAlert } from './useAlert';
|
||||||
|
import { useWorkspaces } from './useWorkspaces';
|
||||||
|
|
||||||
export function useExportData() {
|
export function useExportData() {
|
||||||
const workspace = useActiveWorkspace();
|
const workspaces = useWorkspaces();
|
||||||
|
const activeWorkspace = useActiveWorkspace();
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
|
const dialog = useDialog();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
onError: (err: string) => {
|
onError: (err: string) => {
|
||||||
alert({ id: 'export-failed', title: 'Export Failed', body: err });
|
alert({ id: 'export-failed', title: 'Export Failed', body: err });
|
||||||
},
|
},
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
if (workspace == null) return;
|
if (activeWorkspace == null || workspaces.length === 0) return;
|
||||||
|
|
||||||
const workspaceSlug = slugify(workspace.name, { lower: true });
|
dialog.show({
|
||||||
const exportPath = await save({
|
id: 'export-data',
|
||||||
title: 'Export Data',
|
title: 'Export App Data',
|
||||||
defaultPath: `yaak.${workspaceSlug}.json`,
|
size: 'md',
|
||||||
|
noPadding: true,
|
||||||
|
render: ({ hide }) => (
|
||||||
|
<ExportDataDialog
|
||||||
|
onHide={hide}
|
||||||
|
workspaces={workspaces}
|
||||||
|
activeWorkspace={activeWorkspace}
|
||||||
|
/>
|
||||||
|
),
|
||||||
});
|
});
|
||||||
if (exportPath == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await invoke('cmd_export_data', { workspaceId: workspace.id, exportPath });
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,16 @@ export function pluralize(word: string, count: number): string {
|
|||||||
return `${word}s`;
|
return `${word}s`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function count(word: string, count: number): string {
|
export function count(
|
||||||
|
word: string,
|
||||||
|
count: number,
|
||||||
|
opt: { omitSingle?: boolean; noneWord?: string } = {},
|
||||||
|
): string {
|
||||||
|
if (opt.omitSingle && count === 1) {
|
||||||
|
return word;
|
||||||
|
}
|
||||||
|
if (opt.noneWord && count === 0) {
|
||||||
|
return opt.noneWord;
|
||||||
|
}
|
||||||
return `${count} ${pluralize(word, count)}`;
|
return `${count} ${pluralize(word, count)}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,7 @@
|
|||||||
@apply w-full h-full overflow-hidden text-gray-900 bg-gray-50;
|
@apply w-full h-full overflow-hidden text-gray-900 bg-gray-50;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Setup default transitions for elements */
|
|
||||||
* {
|
* {
|
||||||
transition: background-color var(--transition-duration), border-color var(--transition-duration),
|
|
||||||
box-shadow var(--transition-duration);
|
|
||||||
font-variant-ligatures: none;
|
font-variant-ligatures: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user