diff --git a/src/lib/components/duplicate-workspace-dialog.svelte b/src/lib/components/duplicate-workspace-dialog.svelte
new file mode 100644
index 0000000..3005f34
--- /dev/null
+++ b/src/lib/components/duplicate-workspace-dialog.svelte
@@ -0,0 +1,338 @@
+
+
+
+
+
+
+
+ Duplicate Workspace
+
+
+ Create a copy of "{sourceWorkspace?.Name}" with all its collections and
+ requests.
+
+
+
+
+
+
diff --git a/src/lib/components/settings-dialog.svelte b/src/lib/components/settings-dialog.svelte
new file mode 100644
index 0000000..98ab306
--- /dev/null
+++ b/src/lib/components/settings-dialog.svelte
@@ -0,0 +1,262 @@
+
+
+
+
+
+
+
+ Settings
+
+
+ Configure application settings and preferences.
+
+
+
+
+
+ General
+ Requests
+ Advanced
+
+
+
+
+
+
+
+
+ {settings.theme === "light"
+ ? "Light"
+ : settings.theme === "dark"
+ ? "Dark"
+ : "System"}
+
+
+ System
+ Light
+ Dark
+
+
+
+ Choose the application color theme.
+
+
+
+
+
+
+
+
+
+ Maximum number of request history items to keep.
+
+
+
+
+
+
+
+ Automatically save request changes.
+
+
+
+
+
+
+
+
+
+
+
+ Default timeout for HTTP requests in milliseconds.
+
+
+
+
+
+
+
+
+
+ Automatically follow HTTP redirects.
+
+
+
+
+
+
+
+
+
+ Verify SSL/TLS certificates for HTTPS requests.
+
+
+
+
+
+
+
+
+
Reset Settings
+
+ Reset all settings to their default values. This action cannot be
+ undone.
+
+
+
+
+
+
Data Management
+
+ Export or import your workspaces, collections, and settings.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/lib/components/variables-panel.svelte b/src/lib/components/variables-panel.svelte
new file mode 100644
index 0000000..5714692
--- /dev/null
+++ b/src/lib/components/variables-panel.svelte
@@ -0,0 +1,418 @@
+
+
+
+
+
+
+
+ Variables & Secrets
+
+
+ Manage variables and secrets at different scopes. Variables can be used
+ in requests with {"{{VAR_NAME}}"} syntax.
+
+
+
+
+
+ Global
+ Workspace
+ Collection
+
+
+
+
+
+
+ Global variables are available in all workspaces.
+
+
+
+ {#if globalVariables.length === 0}
+
+ No global variables defined.
+
+ {:else}
+ {#each globalVariables as variable (variable.id)}
+ {@render variableRow(variable)}
+ {/each}
+ {/if}
+
+
+
+
+
+ Workspace variables are available in this workspace only.
+
+
+
+ {#if workspaceVariables.length === 0}
+
+ No workspace variables defined.
+
+ {:else}
+ {#each workspaceVariables as variable (variable.id)}
+ {@render variableRow(variable)}
+ {/each}
+ {/if}
+
+
+
+
+
+ Collection variables are available in this collection only.
+
+
+
+ {#if collectionVariables.length === 0}
+
+ No collection variables defined.
+
+ {:else}
+ {#each collectionVariables as variable (variable.id)}
+ {@render variableRow(variable)}
+ {/each}
+ {/if}
+
+
+
+
+
+
+
+
+
+
+{#snippet variableRow(variable: Variable)}
+
+
+
+ {variable.name}
+ {#if variable.isSecret}
+
+ {/if}
+
+ {variable.scope}
+
+
+
+
+ {#if variable.isSecret && !showSecretValues.has(variable.id)}
+ {getMaskedValue(variable.value)}
+ {:else}
+ {variable.value}
+ {/if}
+
+ {#if variable.isSecret}
+
+ {/if}
+
+ {#if variable.description}
+
{variable.description}
+ {/if}
+
+
+
+
+
+
+{/snippet}
+
+
+
+
+
+ {isCreating ? "Create Variable" : "Edit Variable"}
+
+
+ {isCreating
+ ? "Add a new variable or secret."
+ : "Update variable details."}
+
+
+
+
+
diff --git a/src/lib/services/settings.ts b/src/lib/services/settings.ts
new file mode 100644
index 0000000..a1a047c
--- /dev/null
+++ b/src/lib/services/settings.ts
@@ -0,0 +1,33 @@
+import type { AppSettings } from "$lib/types/workspace";
+
+let settings: AppSettings = {
+ theme: "system",
+ defaultTimeout: 30000,
+ followRedirects: true,
+ validateSSL: true,
+ maxHistoryItems: 100,
+ autoSaveRequests: true,
+};
+
+export async function get_settings(): Promise {
+ return { ...settings };
+}
+
+export async function update_settings(
+ updates: Partial
+): Promise {
+ settings = { ...settings, ...updates };
+ return { ...settings };
+}
+
+export async function reset_settings(): Promise {
+ settings = {
+ theme: "system",
+ defaultTimeout: 30000,
+ followRedirects: true,
+ validateSSL: true,
+ maxHistoryItems: 100,
+ autoSaveRequests: true,
+ };
+ return { ...settings };
+}
diff --git a/src/lib/services/sync.ts b/src/lib/services/sync.ts
new file mode 100644
index 0000000..9340c1c
--- /dev/null
+++ b/src/lib/services/sync.ts
@@ -0,0 +1,278 @@
+import type { Workspace, WorkspaceSyncGroup } from "$lib/types/workspace";
+import type { Collection } from "$lib/types/collection";
+import type { Request } from "$lib/types/request";
+import type { Variable } from "$lib/types/variable";
+import {
+ get_collections_by_workspace,
+ get_standalone_requests_by_workspace,
+ create_collection,
+ create_request,
+} from "./collections";
+import {
+ get_workspace_variables,
+ create_variable,
+ update_variable,
+ get_variables_by_scope,
+} from "./variables";
+
+const syncGroups: Map = new Map();
+
+export async function get_sync_groups(): Promise {
+ return [...syncGroups.values()];
+}
+
+export async function get_sync_group(
+ id: string
+): Promise {
+ return syncGroups.get(id) ?? null;
+}
+
+export async function get_sync_group_for_workspace(
+ workspaceId: string
+): Promise {
+ for (const group of syncGroups.values()) {
+ if (group.workspaceIds.includes(workspaceId)) {
+ return group;
+ }
+ }
+ return null;
+}
+
+export async function create_sync_group(
+ name: string,
+ workspaceIds: string[],
+ syncedVariableNames: string[] = [],
+ syncSecrets: boolean = false
+): Promise {
+ const group: WorkspaceSyncGroup = {
+ id: crypto.randomUUID(),
+ name,
+ workspaceIds,
+ syncedVariableNames,
+ syncSecrets,
+ };
+ syncGroups.set(group.id, group);
+ return group;
+}
+
+export async function update_sync_group(
+ id: string,
+ updates: Partial>
+): Promise {
+ const group = syncGroups.get(id);
+ if (!group) return false;
+ Object.assign(group, updates);
+ return true;
+}
+
+export async function delete_sync_group(id: string): Promise {
+ return syncGroups.delete(id);
+}
+
+export async function add_workspace_to_sync_group(
+ groupId: string,
+ workspaceId: string
+): Promise {
+ const group = syncGroups.get(groupId);
+ if (!group) return false;
+ if (!group.workspaceIds.includes(workspaceId)) {
+ group.workspaceIds.push(workspaceId);
+ }
+ return true;
+}
+
+export async function remove_workspace_from_sync_group(
+ groupId: string,
+ workspaceId: string
+): Promise {
+ const group = syncGroups.get(groupId);
+ if (!group) return false;
+ group.workspaceIds = group.workspaceIds.filter((id) => id !== workspaceId);
+ return true;
+}
+
+export type DuplicateWorkspaceOptions = {
+ sourceWorkspaceId: string;
+ newName: string;
+ newDescription: string;
+ environment?: string;
+ copyVariables: boolean;
+ copySecrets: boolean;
+ createSyncGroup: boolean;
+ syncGroupName?: string;
+ variablesToSync?: string[];
+};
+
+export type DuplicateWorkspaceResult = {
+ workspace: Workspace;
+ syncGroup?: WorkspaceSyncGroup;
+};
+
+export async function duplicate_workspace(
+ options: DuplicateWorkspaceOptions,
+ createWorkspaceFn: (workspace: Omit) => Promise
+): Promise {
+ const {
+ sourceWorkspaceId,
+ newName,
+ newDescription,
+ environment,
+ copyVariables,
+ copySecrets,
+ createSyncGroup: shouldCreateSyncGroup,
+ syncGroupName,
+ variablesToSync = [],
+ } = options;
+
+ // Create the new workspace
+ const newWorkspace = await createWorkspaceFn({
+ Name: newName,
+ Description: newDescription,
+ environment,
+ syncGroupId: null,
+ });
+
+ // Copy collections and requests
+ const collections = await get_collections_by_workspace(sourceWorkspaceId);
+ const collectionIdMap = new Map();
+
+ for (const collection of collections) {
+ const newCollection = await create_collection({
+ name: collection.name,
+ description: collection.description,
+ workspaceId: newWorkspace.Id,
+ });
+ collectionIdMap.set(collection.id, newCollection.id);
+
+ // Copy requests in collection
+ for (const request of collection.requests) {
+ await create_request({
+ name: request.name,
+ method: request.method,
+ url: request.url,
+ headers: [...request.headers],
+ params: [...request.params],
+ bodyType: request.bodyType,
+ body: request.body,
+ formData: [...request.formData],
+ collectionId: newCollection.id,
+ workspaceId: newWorkspace.Id,
+ });
+ }
+ }
+
+ // Copy standalone requests
+ const standaloneRequests = await get_standalone_requests_by_workspace(
+ sourceWorkspaceId
+ );
+ for (const request of standaloneRequests) {
+ await create_request({
+ name: request.name,
+ method: request.method,
+ url: request.url,
+ headers: [...request.headers],
+ params: [...request.params],
+ bodyType: request.bodyType,
+ body: request.body,
+ formData: [...request.formData],
+ collectionId: null,
+ workspaceId: newWorkspace.Id,
+ });
+ }
+
+ // Copy variables
+ if (copyVariables) {
+ const sourceVariables = await get_workspace_variables(sourceWorkspaceId);
+ for (const variable of sourceVariables) {
+ // Skip secrets if not copying them
+ if (variable.isSecret && !copySecrets) {
+ // Create variable with empty value for secrets
+ await create_variable({
+ name: variable.name,
+ value: "",
+ scope: "workspace",
+ scopeId: newWorkspace.Id,
+ isSecret: true,
+ description: variable.description,
+ });
+ } else {
+ await create_variable({
+ name: variable.name,
+ value: variable.value,
+ scope: "workspace",
+ scopeId: newWorkspace.Id,
+ isSecret: variable.isSecret,
+ description: variable.description,
+ });
+ }
+ }
+ }
+
+ // Create sync group if requested
+ let syncGroup: WorkspaceSyncGroup | undefined;
+ if (shouldCreateSyncGroup) {
+ syncGroup = await create_sync_group(
+ syncGroupName || `${newName} Sync Group`,
+ [sourceWorkspaceId, newWorkspace.Id],
+ variablesToSync,
+ copySecrets
+ );
+ newWorkspace.syncGroupId = syncGroup.id;
+ }
+
+ return { workspace: newWorkspace, syncGroup };
+}
+
+export async function sync_variables_in_group(
+ groupId: string,
+ sourceWorkspaceId: string
+): Promise<{ synced: number; skipped: number }> {
+ const group = syncGroups.get(groupId);
+ if (!group) return { synced: 0, skipped: 0 };
+
+ const sourceVariables = await get_workspace_variables(sourceWorkspaceId);
+ let synced = 0;
+ let skipped = 0;
+
+ for (const targetWorkspaceId of group.workspaceIds) {
+ if (targetWorkspaceId === sourceWorkspaceId) continue;
+
+ const targetVariables = await get_workspace_variables(targetWorkspaceId);
+ const targetVarMap = new Map(targetVariables.map((v) => [v.name, v]));
+
+ for (const sourceVar of sourceVariables) {
+ // Only sync variables that are in the sync list
+ if (!group.syncedVariableNames.includes(sourceVar.name)) {
+ skipped++;
+ continue;
+ }
+
+ // Skip secrets if not syncing them
+ if (sourceVar.isSecret && !group.syncSecrets) {
+ skipped++;
+ continue;
+ }
+
+ const targetVar = targetVarMap.get(sourceVar.name);
+ if (targetVar) {
+ // Update existing variable
+ await update_variable(targetVar.id, { value: sourceVar.value });
+ synced++;
+ } else {
+ // Create new variable in target workspace
+ await create_variable({
+ name: sourceVar.name,
+ value:
+ sourceVar.isSecret && !group.syncSecrets ? "" : sourceVar.value,
+ scope: "workspace",
+ scopeId: targetWorkspaceId,
+ isSecret: sourceVar.isSecret,
+ description: sourceVar.description,
+ });
+ synced++;
+ }
+ }
+ }
+
+ return { synced, skipped };
+}
diff --git a/src/lib/services/workspaces.ts b/src/lib/services/workspaces.ts
index 6aa0b3e..0177c9a 100644
--- a/src/lib/services/workspaces.ts
+++ b/src/lib/services/workspaces.ts
@@ -7,6 +7,8 @@ const ws: Map = new Map([
Id: "1",
Name: "Test 1",
Description: "This is a test description",
+ environment: "Development",
+ syncGroupId: null,
},
],
[
@@ -15,6 +17,8 @@ const ws: Map = new Map([
Id: "2",
Name: "Test 2",
Description: "This is a longer test description",
+ environment: "Production",
+ syncGroupId: null,
},
],
[
@@ -23,6 +27,7 @@ const ws: Map = new Map([
Id: "3",
Name: "Test 3",
Description: "This is a slightly longer test description",
+ syncGroupId: null,
},
],
[
@@ -31,6 +36,7 @@ const ws: Map = new Map([
Id: "4",
Name: "Test 4",
Description: "This is an even slightly longer test description",
+ syncGroupId: null,
},
],
[
@@ -40,6 +46,7 @@ const ws: Map = new Map([
Name: "Test 5",
Description:
"This is a veryyyyyyyyyyyyyyyyyyyyyyyyyyy longggggggggggggggggggggggggggg test descriptionnnnnnnnnnnnnnnnnnnnnnnnnnnnnn",
+ syncGroupId: null,
},
],
]);
@@ -67,8 +74,23 @@ export async function update_workspace(workspace: Workspace): Promise {
return true;
}
-export async function create_workspace(workspace: Workspace): Promise {
- workspace.Id = crypto.randomUUID();
- ws.set(workspace.Id, workspace);
- return true;
+export async function create_workspace(
+ workspace: Omit
+): Promise {
+ const newWorkspace: Workspace = {
+ ...workspace,
+ Id: crypto.randomUUID(),
+ };
+ ws.set(newWorkspace.Id, newWorkspace);
+ return newWorkspace;
+}
+
+export async function delete_workspace(id: string): Promise {
+ return ws.delete(id);
+}
+
+export async function get_workspaces_by_sync_group(
+ syncGroupId: string
+): Promise {
+ return [...ws.values()].filter((w) => w.syncGroupId === syncGroupId);
}
diff --git a/src/lib/types/workspace.ts b/src/lib/types/workspace.ts
index da7331a..58426d5 100644
--- a/src/lib/types/workspace.ts
+++ b/src/lib/types/workspace.ts
@@ -2,4 +2,23 @@ export type Workspace = {
Id: string;
Name: string;
Description: string;
+ syncGroupId?: string | null;
+ environment?: string;
+};
+
+export type WorkspaceSyncGroup = {
+ id: string;
+ name: string;
+ workspaceIds: string[];
+ syncedVariableNames: string[];
+ syncSecrets: boolean;
+};
+
+export type AppSettings = {
+ theme: "light" | "dark" | "system";
+ defaultTimeout: number;
+ followRedirects: boolean;
+ validateSSL: boolean;
+ maxHistoryItems: number;
+ autoSaveRequests: boolean;
};
diff --git a/src/routes/workspaces/+page.svelte b/src/routes/workspaces/+page.svelte
index 0a5429d..20978cb 100644
--- a/src/routes/workspaces/+page.svelte
+++ b/src/routes/workspaces/+page.svelte
@@ -1,30 +1,49 @@
@@ -107,7 +150,25 @@
Workspaces
-
+
+
+
+
+
-
- {workspace.Name}
-
+
+
+
+
+ {#snippet child({ props })}
+
+ {/snippet}
+
+
+ openEditDialog(workspace)}
+ >
+
+ Edit
+
+ openDuplicateDialog(workspace)}
+ >
+
+ Duplicate
+
+
+ openDeleteDialog(workspace)}
+ >
+
+ Delete
+
+
+
+
+ {workspace.Name}
+
{workspace.Description}
+ {#if workspace.environment}
+ {workspace.environment}
+ {/if}
-
@@ -181,3 +289,40 @@
+
+
+
+
+ Delete Workspace
+
+ Are you sure you want to delete "{workspaceToDelete?.Name}"? This will
+ permanently delete all collections, requests, and variables in this
+ workspace.
+
+
+
+
+
+
+
+
+
+ (settingsOpen = v)}
+/>
+
+ (variablesOpen = v)}
+/>
+
+ (duplicateDialogOpen = v)}
+ sourceWorkspace={workspaceToDuplicate}
+ onDuplicate={handleDuplicate}
+/>
diff --git a/src/routes/workspaces/[id]/+page.svelte b/src/routes/workspaces/[id]/+page.svelte
index ace3479..7b97d6d 100644
--- a/src/routes/workspaces/[id]/+page.svelte
+++ b/src/routes/workspaces/[id]/+page.svelte
@@ -10,7 +10,11 @@
import RequestPanel from "$lib/components/request-panel.svelte";
import * as Empty from "$lib/components/ui/empty/index.js";
import SendIcon from "@lucide/svelte/icons/send";
+ import VariableIcon from "@lucide/svelte/icons/variable";
+ import SettingsIcon from "@lucide/svelte/icons/settings";
import { get_workspace } from "$lib/services/workspaces";
+ import VariablesPanel from "$lib/components/variables-panel.svelte";
+ import SettingsDialog from "$lib/components/settings-dialog.svelte";
import {
get_collections_by_workspace,
get_standalone_requests_by_workspace,
@@ -39,6 +43,9 @@
let loading = $state(false);
let resolvedVariables = $state([]);
+ let variablesOpen = $state(false);
+ let settingsOpen = $state(false);
+
let collectionDialogOpen = $state(false);
let collectionDialogMode = $state<"create" | "edit">("create");
let editingCollection = $state(null);
@@ -280,6 +287,24 @@
{/if}
+
+
+
+
@@ -412,3 +437,15 @@
+
+ (variablesOpen = v)}
+ workspaceId={params.id}
+ collectionId={selectedRequest?.collectionId ?? undefined}
+/>
+
+ (settingsOpen = v)}
+/>