diff --git a/.gitignore b/.gitignore index c6f0ee20..0c00733f 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,7 @@ typings/ # Optional npm cache directory .npm +package-lock.json # Optional eslint cache .eslintcache diff --git a/src/main/environment/service/environment-service.ts b/src/main/environment/service/environment-service.ts index a1431664..53a02944 100644 --- a/src/main/environment/service/environment-service.ts +++ b/src/main/environment/service/environment-service.ts @@ -7,6 +7,7 @@ import { Collection } from 'shim/objects/collection'; import { VariableMap } from 'shim/objects/variables'; import { getSystemVariable, getSystemVariables } from './system-variable'; import { SettingsService } from 'main/persistence/service/settings-service'; +import { EnvironmentMap } from 'shim/objects/environment'; const persistenceService = PersistenceService.instance; const settingsService = SettingsService.instance; @@ -60,6 +61,10 @@ export class EnvironmentService implements Initializable { this.currentCollection.variables = variables; } + public setEnvironmentVariables(environmentVariables: EnvironmentMap) { + this.currentCollection.environments = environmentVariables; + } + /** * Loads the collection at the specified path and sets it as the current collection. * diff --git a/src/main/event/main-event-service.ts b/src/main/event/main-event-service.ts index 26dbea4e..4fa20be5 100644 --- a/src/main/event/main-event-service.ts +++ b/src/main/event/main-event-service.ts @@ -8,6 +8,7 @@ import { TrufosObject } from 'shim/objects'; import { EnvironmentService } from 'main/environment/service/environment-service'; import { VariableMap } from 'shim/objects/variables'; import { Folder } from 'shim/objects/folder'; +import { EnvironmentMap } from 'shim/objects/environment'; const persistenceService = PersistenceService.instance; const environmentService = EnvironmentService.instance; @@ -113,6 +114,11 @@ export class MainEventService implements IEventService { await persistenceService.saveCollection(environmentService.currentCollection); } + async setEnvironmentVariables(environmentVariables: EnvironmentMap) { + environmentService.setEnvironmentVariables(environmentVariables); + await persistenceService.saveCollection(environmentService.currentCollection); + } + async selectEnvironment(key: string) { environmentService.currentEnvironmentKey = key; } diff --git a/src/main/main.ts b/src/main/main.ts index 5a3e3678..4e9de53e 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -30,6 +30,7 @@ const createWindow = async () => { preload: path.join(__dirname, 'preload.js'), }, }); + mainWindow.setFullScreen(true); // Open links in default browser mainWindow.webContents.setWindowOpenHandler(({ url }) => { diff --git a/src/renderer/components/mainWindow/mainTopBar/HttpMethodSelect.tsx b/src/renderer/components/mainWindow/mainTopBar/HttpMethodSelect.tsx index 83d1b28a..0b452651 100644 --- a/src/renderer/components/mainWindow/mainTopBar/HttpMethodSelect.tsx +++ b/src/renderer/components/mainWindow/mainTopBar/HttpMethodSelect.tsx @@ -1,4 +1,4 @@ -import { FC, useEffect, useRef, useCallback, useState } from 'react'; +import { FC, useEffect, useRef, useCallback, useState, JSX } from 'react'; import { Select, SelectContent, diff --git a/src/renderer/components/shared/modal/NamingModal.tsx b/src/renderer/components/shared/modal/NamingModal.tsx new file mode 100644 index 00000000..47e0083c --- /dev/null +++ b/src/renderer/components/shared/modal/NamingModal.tsx @@ -0,0 +1,57 @@ +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { FormEvent } from 'react'; + +interface NamingModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + modalTitle: string; + onSubmit: () => void; + onReset: () => void; + value: string; + onChange: (value: string) => void; + placeholder: string; + valid: boolean; +} + +export const NamingModal = (props: NamingModalProps) => { + const onSubmit = (event: FormEvent) => { + event.preventDefault(); + props.onSubmit(); + }; + + return ( + + + + {props.modalTitle} + +
+
+ props.onChange(e.target.value)} + className="w-full bg-transparent outline-none" + placeholder={props.placeholder} + /> +
+ + + + +
+
+
+ ); +}; diff --git a/src/renderer/components/shared/settings/SettingsModal.tsx b/src/renderer/components/shared/settings/SettingsModal.tsx index 546a8ee2..799213be 100644 --- a/src/renderer/components/shared/settings/SettingsModal.tsx +++ b/src/renderer/components/shared/settings/SettingsModal.tsx @@ -7,32 +7,48 @@ import { DialogTrigger, } from '@/components/ui/dialog'; import { FiSettings } from 'react-icons/fi'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import * as React from 'react'; +import { useState } from 'react'; +import { + selectCollectionVariables, + selectEnvironmentVariables, + useVariableActions, + useVariableStore, +} from '@/state/variableStore'; +import { CollectionVariableTab } from '@/components/shared/settings/variableTabs/CollectionVariableTab'; import { variableArrayToMap, - VariableEditor, variableMapToArray, -} from '@/components/shared/settings/VariableTab/VariableEditor'; -import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; -import { Button } from '@/components/ui/button'; -import * as React from 'react'; -import { useState } from 'react'; -import { selectVariables, useVariableActions, useVariableStore } from '@/state/variableStore'; +} from '@/components/shared/settings/variableTabs/helper/EditVariableHelper'; +import { EnvironmentVariableTab } from '@/components/shared/settings/variableTabs/EnvironmentVariableTab'; +import { SettingsModalFooter } from '@/components/shared/settings/footer/SettingsModalFooter'; export const SettingsModal = () => { - const { setVariables } = useVariableActions(); - const variables = useVariableStore((state) => selectVariables(state)); - const [editorVariables, setEditorVariables] = useState(variableMapToArray(variables)); + const actions = useVariableActions(); + const collectionStoreVariables = useVariableStore((state) => selectCollectionVariables(state)); + const environmentStoreVariables = useVariableStore((state) => selectEnvironmentVariables(state)); + + const [collectionVariables, setCollectionVariables] = useState( + variableMapToArray(collectionStoreVariables) + ); + const [environmentVariables, setEnvironmentVariables] = useState(environmentStoreVariables); const [isValid, setValid] = useState(false); - const [isOpen, setOpen] = useState(false); + const [isOpen, setOpen] = useState(true); const save = async () => { - console.info('Saving variables:', editorVariables); - await setVariables(variableArrayToMap(editorVariables)); + await apply(); setOpen(false); }; + const apply = async () => { + await actions.setCollectionVariables(variableArrayToMap(collectionVariables)); + await actions.setEnvironmentVariables(environmentVariables); + }; + const cancel = () => { - setEditorVariables(variableMapToArray(variables)); + setCollectionVariables(variableMapToArray(collectionStoreVariables)); + setEnvironmentVariables(environmentStoreVariables); setOpen(false); }; @@ -45,30 +61,28 @@ export const SettingsModal = () => { Settings - + - Variables + CollectionVariables + EnvironmentVariables - - + + + + - - + diff --git a/src/renderer/components/shared/settings/VariableTab/VariableEditor.tsx b/src/renderer/components/shared/settings/VariableTab/VariableEditor.tsx deleted file mode 100644 index 2f1cf571..00000000 --- a/src/renderer/components/shared/settings/VariableTab/VariableEditor.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import { Divider } from '@/components/shared/Divider'; -import { Button } from '@/components/ui/button'; -import { AddIcon, DeleteIcon } from '@/components/icons'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table'; -import { VARIABLE_NAME_REGEX, VariableMap, VariableObject } from 'shim/objects/variables'; -import { memo, useEffect } from 'react'; -import { produce } from 'immer'; - -export interface VariableEditorProps { - variables: VariableObjectWithKey[]; - onValidChange?: (valid: boolean) => void; - onVariablesChange?: (variables: VariableObjectWithKey[]) => void; -} - -type VariableObjectWithKey = VariableObject & { key: string }; - -export function variableMapToArray(map: VariableMap) { - return Object.entries(map).map(([key, variable]) => ({ - key, - ...variable, - })); -} - -export function variableArrayToMap(array: VariableObjectWithKey[]) { - return Object.fromEntries(array.map(({ key, ...variable }) => [key, variable])); -} - -function getInvalidVariableKeys(variables: VariableObjectWithKey[]) { - const allKeys = new Set(); - const invalidKeys = new Set(); - for (const { key } of variables) { - if (key === '' || key.length > 255 || allKeys.has(key) || !VARIABLE_NAME_REGEX.test(key)) { - invalidKeys.add(key); - } - allKeys.add(key); - } - - return invalidKeys; -} - -/** A component that allows adding, editing and removing variables */ -export const VariableEditor = memo( - ({ variables, onValidChange, onVariablesChange }) => { - // default props - onValidChange ??= () => {}; - onVariablesChange ??= () => {}; - - // state handling - const invalidVariableKeys = getInvalidVariableKeys(variables); - const isValid = invalidVariableKeys.size === 0; - - // callbacks - useEffect(() => onValidChange(isValid), [isValid]); - - const add = () => { - if (invalidVariableKeys.has('')) return; - onVariablesChange( - produce(variables, (variables) => { - variables.push({ key: '', value: '' }); - }) - ); - }; - - const update = (index: number, updatedFields: Partial) => { - onVariablesChange( - produce(variables, (variables) => { - variables[index] = { ...variables[index], ...updatedFields }; - }) - ); - }; - - const remove = (index: number) => { - onVariablesChange( - produce(variables, (variables) => { - variables.splice(index, 1); - }) - ); - }; - - return ( -
-
-
- -
- -
- -
- - - - Key - Value - Description - {/* Action Column */} - - - - {variables.map((variable, index) => ( - - - update(index, { key: e.target.value })} - /> - - - update(index, { value: e.target.value })} - /> - - - update(index, { description: e.target.value })} - /> - - -
- -
-
-
- ))} -
-
-
-
- ); - } -); diff --git a/src/renderer/components/shared/settings/footer/SettingsModalFooter.tsx b/src/renderer/components/shared/settings/footer/SettingsModalFooter.tsx new file mode 100644 index 00000000..2f08f813 --- /dev/null +++ b/src/renderer/components/shared/settings/footer/SettingsModalFooter.tsx @@ -0,0 +1,33 @@ +import { Button } from '@/components/ui/button'; +import * as React from 'react'; + +export const SettingsModalFooter = (props: { + save: () => Promise; + valid: boolean; + apply: () => Promise; + cancel: () => void; +}) => { + return ( +
+ + + +
+ ); +}; diff --git a/src/renderer/components/shared/settings/variableTabs/CollectionVariableTab.tsx b/src/renderer/components/shared/settings/variableTabs/CollectionVariableTab.tsx new file mode 100644 index 00000000..cbe98814 --- /dev/null +++ b/src/renderer/components/shared/settings/variableTabs/CollectionVariableTab.tsx @@ -0,0 +1,23 @@ +import { VariableEditor } from '@/components/shared/settings/variableTabs/table/VariableEditor'; +import { VariableObjectWithKey } from 'shim/objects/variables'; + +export interface CollectionVariableTabProps { + variables: VariableObjectWithKey[]; + onValidChange?: (valid: boolean) => void; + onVariablesChange?: (variables: VariableObjectWithKey[]) => void; +} + +export const CollectionVariableTab = ({ + variables, + onValidChange, + onVariablesChange, +}: CollectionVariableTabProps) => { + return ( + + ); +}; diff --git a/src/renderer/components/shared/settings/variableTabs/EnvironmentVariableTab.tsx b/src/renderer/components/shared/settings/variableTabs/EnvironmentVariableTab.tsx new file mode 100644 index 00000000..99185157 --- /dev/null +++ b/src/renderer/components/shared/settings/variableTabs/EnvironmentVariableTab.tsx @@ -0,0 +1,107 @@ +import * as React from 'react'; +import { useEffect, useState } from 'react'; +import { + Sidebar, + SidebarHeader, + SidebarMenuButton, + SidebarMenuSubButton, + SidebarProvider, +} from '@/components/ui/sidebar'; +import { EnvironmentMap } from 'shim/objects/environment'; +import { + variableArrayToMap, + variableMapToArray, +} from '@/components/shared/settings/variableTabs/helper/EditVariableHelper'; +import { VariableEditor } from '@/components/shared/settings/variableTabs/table/VariableEditor'; +import { VariableObjectWithKey } from 'shim/objects/variables'; +import { CreateEnvironmentModal } from '@/components/shared/settings/variableTabs/modal/CreateEnvironmentModal'; +import { EnvironmentDropdown } from '@/components/shared/settings/variableTabs/dropdown/EnvironmentDropdown'; + +export interface EnvironmentVariableEditorProps { + environments: EnvironmentMap; + onValidChange?: (valid: boolean) => void; + onEnvironmentChange?: (variables: EnvironmentMap) => void; +} + +/** A component that allows adding, editing and removing variables */ +export const EnvironmentVariableTab = ({ + environments, + onEnvironmentChange, + onValidChange, +}: EnvironmentVariableEditorProps) => { + const environmentList = Object.keys(environments); + const firstEnvironment = environments[environmentList[0]] ?? {}; + const [editorEnvironmentVariables, setEditorEnvironmentVariables] = useState( + variableMapToArray(firstEnvironment) + ); + const [selectedEnvironment, setSelectedEnvironment] = useState(environmentList[0]); + + const onVariableChange = (variables: VariableObjectWithKey[]) => { + const variableMap = variableArrayToMap(variables); + const updatedEnvironment = { + ...environments, + [selectedEnvironment]: variableMap, + } as EnvironmentMap; + setEditorEnvironmentVariables(variables); + onEnvironmentChange(updatedEnvironment); + }; + + const [createModalIsOpen, setCreateModalIsOpen] = useState(false); + + useEffect(() => { + const selectedEnvironmentVariables = environments[selectedEnvironment] ?? {}; + setEditorEnvironmentVariables(variableMapToArray(selectedEnvironmentVariables)); + }, [selectedEnvironment, environments]); + + return ( +
+ + + Environments + {environmentList.map((environment) => { + const [hover, setHover] = useState(false); + + return ( + setHover(true)} + onMouseLeave={() => setHover(false)} + isActive={environment === selectedEnvironment} + onClick={() => setSelectedEnvironment(environment)} + key={environment} + > + {environment} + + + ); + })} + setCreateModalIsOpen(true)}> + + Add Environment + + + + + +
+ ); +}; diff --git a/src/renderer/components/shared/settings/variableTabs/dropdown/EnvironmentDropdown.tsx b/src/renderer/components/shared/settings/variableTabs/dropdown/EnvironmentDropdown.tsx new file mode 100644 index 00000000..c4f19891 --- /dev/null +++ b/src/renderer/components/shared/settings/variableTabs/dropdown/EnvironmentDropdown.tsx @@ -0,0 +1,33 @@ +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { SidebarMenuAction } from '@/components/ui/sidebar'; +import { MoreHorizontal } from 'lucide-react'; + +export interface RequestDropdownProps { + environmentKey: string; + selected: boolean; + hover: boolean; +} + +export const EnvironmentDropdown = ({ environmentKey, selected, hover }: RequestDropdownProps) => { + return ( +
+ + + + + More + + + + Rename Request + Delete Request + + +
+ ); +}; diff --git a/src/renderer/components/shared/settings/variableTabs/helper/EditVariableHelper.ts b/src/renderer/components/shared/settings/variableTabs/helper/EditVariableHelper.ts new file mode 100644 index 00000000..1193ce8c --- /dev/null +++ b/src/renderer/components/shared/settings/variableTabs/helper/EditVariableHelper.ts @@ -0,0 +1,30 @@ +import { + VARIABLE_NAME_REGEX, + VariableMap, + VariableObject, + VariableObjectWithKey, +} from 'shim/objects/variables'; + +export function variableMapToArray(map: VariableMap) { + return Object.entries(map).map(([key, variable]) => ({ + key, + ...variable, + })); +} + +export function variableArrayToMap(array: VariableObjectWithKey[]) { + return Object.fromEntries(array.map(({ key, ...variable }) => [key, variable])); +} + +export function getInvalidVariableKeys(variables: VariableObjectWithKey[]) { + const allKeys = new Set(); + const invalidKeys = new Set(); + for (const { key } of variables) { + if (key === '' || key.length > 255 || allKeys.has(key) || !VARIABLE_NAME_REGEX.test(key)) { + invalidKeys.add(key); + } + allKeys.add(key); + } + + return invalidKeys; +} diff --git a/src/renderer/components/shared/settings/variableTabs/modal/CreateEnvironmentModal.tsx b/src/renderer/components/shared/settings/variableTabs/modal/CreateEnvironmentModal.tsx new file mode 100644 index 00000000..e0646e08 --- /dev/null +++ b/src/renderer/components/shared/settings/variableTabs/modal/CreateEnvironmentModal.tsx @@ -0,0 +1,54 @@ +import { NamingModal } from '@/components/shared/modal/NamingModal'; +import { useEffect, useState } from 'react'; +import { EnvironmentMap } from '../../../../../../shim/objects/environment'; + +interface EnvironmentVariableEditorProps { + isOpen: boolean; + setIsOpen: (open: boolean) => void; + environments: EnvironmentMap; + onEnvironmentChange: (variables: EnvironmentMap) => void; + setSelectedEnvironment: (environment: string) => void; +} + +export const CreateEnvironmentModal = ({ + isOpen, + setIsOpen, + environments, + onEnvironmentChange, + setSelectedEnvironment, +}: EnvironmentVariableEditorProps) => { + const [title, setTitle] = useState(''); + const [isValid, setValid] = useState(false); + + const newEnvironment = () => { + const newEnvironmentName = title ?? (Math.random() + 1).toString(36).substring(7); + setIsOpen(false); + const updatedEnvironment = { + ...environments, + [newEnvironmentName]: {}, + } as EnvironmentMap; + onEnvironmentChange(updatedEnvironment); + setSelectedEnvironment(newEnvironmentName); + }; + + useEffect(() => { + setValid(title.trim().length > 0); + }, [title]); + + return ( + newEnvironment()} + onReset={() => { + setIsOpen(false); + setTitle(''); + }} + value={title} + onChange={(value) => setTitle(value)} + placeholder={'Enter the environment name'} + valid={isValid} + /> + ); +}; diff --git a/src/renderer/components/shared/settings/variableTabs/table/VariableButton.tsx b/src/renderer/components/shared/settings/variableTabs/table/VariableButton.tsx new file mode 100644 index 00000000..6a1a8064 --- /dev/null +++ b/src/renderer/components/shared/settings/variableTabs/table/VariableButton.tsx @@ -0,0 +1,25 @@ +import { Button } from '@/components/ui/button'; +import { Divider } from '@/components/shared/Divider'; +import { AddIcon } from '@/components/icons'; + +interface VariableButtonProps { + add: () => void; +} + +export const VariableButton = ({ add }: VariableButtonProps) => { + return ( +
+
+ +
+ +
+ ); +}; diff --git a/src/renderer/components/shared/settings/variableTabs/table/VariableEditor.tsx b/src/renderer/components/shared/settings/variableTabs/table/VariableEditor.tsx new file mode 100644 index 00000000..4035676b --- /dev/null +++ b/src/renderer/components/shared/settings/variableTabs/table/VariableEditor.tsx @@ -0,0 +1,67 @@ +import { VariableObjectWithKey } from 'shim/objects/variables'; +import { memo, useEffect } from 'react'; +import { produce } from 'immer'; +import { VariableButton } from '@/components/shared/settings/variableTabs/table/VariableButton'; +import { VariableTable } from '@/components/shared/settings/variableTabs/table/VariableTable'; +import { getInvalidVariableKeys } from '@/components/shared/settings/variableTabs/helper/EditVariableHelper'; + +export interface VariableEditorProps { + variables: VariableObjectWithKey[]; + onValidChange?: (valid: boolean) => void; + onVariablesChange?: (variables: VariableObjectWithKey[]) => void; + className?: string; +} + +/** A component that allows adding, editing and removing variables */ +export const VariableEditor = memo( + ({ variables, onValidChange, onVariablesChange, className }) => { + // default props + onValidChange ??= () => {}; + onVariablesChange ??= () => {}; + + // state handling + const invalidVariableKeys = getInvalidVariableKeys(variables); + const isValid = invalidVariableKeys.size === 0; + + // callbacks + useEffect(() => onValidChange(isValid), [isValid]); + + const add = () => { + if (invalidVariableKeys.has('')) return; + onVariablesChange( + produce(variables, (variables) => { + variables.push({ key: '', value: '' }); + }) + ); + }; + + const update = (index: number, updatedFields: Partial) => { + onVariablesChange( + produce(variables, (variables) => { + variables[index] = { ...variables[index], ...updatedFields }; + }) + ); + }; + + const remove = (index: number) => { + onVariablesChange( + produce(variables, (variables) => { + variables.splice(index, 1); + }) + ); + }; + + return ( +
+ + + +
+ ); + } +); diff --git a/src/renderer/components/shared/settings/variableTabs/table/VariableTable.tsx b/src/renderer/components/shared/settings/variableTabs/table/VariableTable.tsx new file mode 100644 index 00000000..c6b6b62c --- /dev/null +++ b/src/renderer/components/shared/settings/variableTabs/table/VariableTable.tsx @@ -0,0 +1,88 @@ +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { Button } from '@/components/ui/button'; +import { DeleteIcon } from '@/components/icons'; +import { VariableObjectWithKey } from 'shim/objects/variables'; +import { cn } from '@/lib/utils'; + +interface VariableTableProps { + variables: VariableObjectWithKey[]; + update: (index: number, updatedFields: Partial) => void; + remove: (index: number) => void; + invalidVariableKeys: Set; + className?: string; +} + +export const VariableTable = ({ + variables, + update, + remove, + invalidVariableKeys, + className, +}: VariableTableProps) => { + return ( +
+ + + + Key + Value + Description + {/* Action Column */} + + + + {variables.map((variable, index) => ( + + + update(index, { key: e.target.value })} + /> + + + update(index, { value: e.target.value })} + /> + + + update(index, { description: e.target.value })} + /> + + +
+ +
+
+
+ ))} +
+
+
+ ); +}; diff --git a/src/renderer/components/sidebar/SidebarHeaderBar.tsx b/src/renderer/components/sidebar/SidebarHeaderBar.tsx index 29da95a5..07d1d79c 100644 --- a/src/renderer/components/sidebar/SidebarHeaderBar.tsx +++ b/src/renderer/components/sidebar/SidebarHeaderBar.tsx @@ -3,7 +3,7 @@ import { SidebarHeader } from '@/components/ui/sidebar'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; import { AddIcon } from '@/components/icons'; -import { NamingModal } from '@/components/sidebar/SidebarRequestList/Nav/Dropdown/modals/NamingModal'; +import { RequestFolderNamingModal } from '@/components/sidebar/SidebarRequestList/Nav/Dropdown/modals/RequestFolderNamingModal'; import { useCollectionStore } from '@/state/collectionStore'; export const SidebarHeaderBar = () => { @@ -43,7 +43,7 @@ export const SidebarHeaderBar = () => { {modalState.isOpen && ( - { {renameModalIsOpen && ( - { {renameModalIsOpen && ( - void; -} - -export const NamingModal = ({ createType, trufosObject, isOpen, setOpen }: NamingModalProps) => { - const [isValid, setValid] = useState(false); - const [name, setName] = useState(createType ? '' : trufosObject.title); - const { renameFolder, renameRequest, addNewRequest, addNewFolder } = useCollectionActions(); - const siblingTitles: string[] = []; - if (createType !== null && trufosObject.type !== 'request') { - siblingTitles.push(...trufosObject.children.map((parent) => parent.title)); - } else if (trufosObject.type !== 'collection') { - const parent = useCollectionStore((state) => selectFolder(state, trufosObject.parentId)); - if (parent) { - siblingTitles.push(...parent.children.map((child) => child.title)); - } else { - const collection = useCollectionStore((state) => state.collection); - if (collection.id === trufosObject.parentId) { - siblingTitles.push(...collection.children.map((child) => child.title)); - } - } - } - - const save = async () => { - if (createType) { - if (createType === 'folder') { - addNewFolder(name, trufosObject.id); - } else { - addNewRequest(name, trufosObject.id); - } - } else { - if (trufosObject.type === 'folder') { - renameFolder(trufosObject.id, name); - } else { - renameRequest(trufosObject.id, name); - } - } - setOpen(false); - }; - - const title = createType - ? `Create a new ${createType.charAt(0).toUpperCase() + createType.slice(1)}` - : `Rename the ${trufosObject.type.charAt(0).toUpperCase() + trufosObject.type.slice(1)}`; - - useEffect(() => { - setValid( - name.trim().length > 0 && - name !== (createType ? '' : trufosObject.title) && - siblingTitles.indexOf(name) === -1 - ); - }, [name]); - - return ( - !open && setOpen(false)}> - - - {title} - -
save()} onReset={() => setOpen(false)}> -
- setName(e.target.value)} - className="w-full bg-transparent outline-none" - placeholder={`Enter the ${createType ?? trufosObject.type} name`} - /> -
- - - - -
-
-
- ); -}; diff --git a/src/renderer/components/sidebar/SidebarRequestList/Nav/Dropdown/modals/RequestFolderNamingModal.tsx b/src/renderer/components/sidebar/SidebarRequestList/Nav/Dropdown/modals/RequestFolderNamingModal.tsx new file mode 100644 index 00000000..a4d7f45c --- /dev/null +++ b/src/renderer/components/sidebar/SidebarRequestList/Nav/Dropdown/modals/RequestFolderNamingModal.tsx @@ -0,0 +1,81 @@ +import { useEffect, useState } from 'react'; +import { TrufosRequest } from 'shim/objects/request'; +import { Folder } from 'shim/objects/folder'; +import { selectFolder, useCollectionActions, useCollectionStore } from '@/state/collectionStore'; +import { Collection } from 'shim/objects/collection'; +import { NamingModal } from '@/components/shared/modal/NamingModal'; + +interface RequestFolderNamingModalProps { + createType?: 'folder' | 'request'; + trufosObject: Folder | TrufosRequest | Omit; + isOpen: boolean; + setOpen: (open: boolean) => void; +} + +export const RequestFolderNamingModal = ({ + createType, + trufosObject, + isOpen, + setOpen, +}: RequestFolderNamingModalProps) => { + const [isValid, setValid] = useState(false); + const [title, setTitle] = useState(createType ? '' : trufosObject.title); + const { renameFolder, renameRequest, addNewRequest, addNewFolder } = useCollectionActions(); + const siblingTitles: string[] = []; + if (createType !== null && trufosObject.type !== 'request') { + siblingTitles.push(...trufosObject.children.map((parent) => parent.title)); + } else if (trufosObject.type !== 'collection') { + const parent = useCollectionStore((state) => selectFolder(state, trufosObject.parentId)); + if (parent) { + siblingTitles.push(...parent.children.map((child) => child.title)); + } else { + const collection = useCollectionStore((state) => state.collection); + if (collection.id === trufosObject.parentId) { + siblingTitles.push(...collection.children.map((child) => child.title)); + } + } + } + + const save = async () => { + if (createType) { + if (createType === 'folder') { + addNewFolder(title, trufosObject.id); + } else { + addNewRequest(title, trufosObject.id); + } + } else { + if (trufosObject.type === 'folder') { + renameFolder(trufosObject.id, title); + } else { + renameRequest(trufosObject.id, title); + } + } + setOpen(false); + }; + + const header = createType + ? `Create a new ${createType.charAt(0).toUpperCase() + createType.slice(1)}` + : `Rename the ${trufosObject.type.charAt(0).toUpperCase() + trufosObject.type.slice(1)}`; + + useEffect(() => { + setValid( + title.trim().length > 0 && + title !== (createType ? '' : trufosObject.title) && + siblingTitles.indexOf(title) === -1 + ); + }, [title]); + + return ( + !open && setOpen(false)} + modalTitle={header} + onSubmit={() => save()} + onReset={() => setOpen(false)} + value={title} + onChange={(value) => setTitle(value)} + placeholder={`Enter the ${createType ?? trufosObject.type} name`} + valid={isValid} + /> + ); +}; diff --git a/src/renderer/components/ui/button.tsx b/src/renderer/components/ui/button.tsx index 23678952..5c7a1031 100644 --- a/src/renderer/components/ui/button.tsx +++ b/src/renderer/components/ui/button.tsx @@ -15,6 +15,8 @@ const buttonVariants = cva( outline: 'border border-primary bg-background hover:bg-accent hover:text-accent-foreground', secondary: 'border border-accent-primary bg-accent-tertiary text-accent-primary hover:bg-accent-primary hover:border-accent-primary hover:text-accent-tertiary', + secondaryDisable: + 'border border-accent-primary text-accent-tertiary hover:bg-accent-primary hover:border-accent-primary hover:text-accent-tertiary', ghost: 'bg-transparent hover:text-accent-primary active:text-accent-secondary active:bg-transparent', link: 'text-primary underline-offset-4 hover:underline', diff --git a/src/renderer/services/event/renderer-event-service.ts b/src/renderer/services/event/renderer-event-service.ts index ecd9a514..b15f2e45 100644 --- a/src/renderer/services/event/renderer-event-service.ts +++ b/src/renderer/services/event/renderer-event-service.ts @@ -52,6 +52,7 @@ export class RendererEventService implements IEventService { getActiveEnvironmentVariables = createEventMethod('getActiveEnvironmentVariables'); getVariable = createEventMethod('getVariable'); setCollectionVariables = createEventMethod('setCollectionVariables'); + setEnvironmentVariables = createEventMethod('setEnvironmentVariables'); saveFolder = createEventMethod('saveFolder'); selectEnvironment = createEventMethod('selectEnvironment'); openCollection = createEventMethod('openCollection'); diff --git a/src/renderer/state/collectionStore.ts b/src/renderer/state/collectionStore.ts index 8a7267b2..27b807db 100644 --- a/src/renderer/state/collectionStore.ts +++ b/src/renderer/state/collectionStore.ts @@ -55,7 +55,7 @@ export const useCollectionStore = create { const requests = new Map(); const folders = new Map(); - const { initialize } = useVariableStore.getState(); + const { initialize } = useVariableStore.getState(); // useVariableActions is not loaded yet const stack = [...collection.children]; while (stack.length > 0) { @@ -69,7 +69,7 @@ export const useCollectionStore = create; + setCollectionVariables(variables: VariableMap): Promise; + + setEnvironmentVariables(environmentVariables: EnvironmentMap): Promise; } diff --git a/src/renderer/state/variableStore.ts b/src/renderer/state/variableStore.ts index 3195e4a2..ff2dd29d 100644 --- a/src/renderer/state/variableStore.ts +++ b/src/renderer/state/variableStore.ts @@ -4,32 +4,44 @@ import { immer } from 'zustand/middleware/immer'; import { VariableMap } from 'shim/objects/variables'; import { RendererEventService } from '@/services/event/renderer-event-service'; import { useActions } from '@/state/helper/util'; +import { EnvironmentMap } from 'shim/objects/environment'; const eventService = RendererEventService.instance; interface VariableState { /** The variables of the current collection */ - variables: VariableMap; + collectionVariables: VariableMap; + environmentVariables: EnvironmentMap; } export const useVariableStore = create()( immer((set, get) => ({ - variables: {}, + collectionVariables: {} as VariableMap, + environmentVariables: {} as EnvironmentMap, - initialize(variables: VariableMap) { + initialize(collectionVariables: VariableMap, environmentVariables: EnvironmentMap) { set((state) => { - state.variables = variables; + state.collectionVariables = collectionVariables; + state.environmentVariables = environmentVariables; }); }, - setVariables: async (variables) => { + setCollectionVariables: async (variables) => { await eventService.setCollectionVariables(variables); set((state) => { - state.variables = variables; + state.collectionVariables = variables; + }); + }, + + setEnvironmentVariables: async (environmentVariables) => { + await eventService.setEnvironmentVariables(environmentVariables); + set((state) => { + state.environmentVariables = environmentVariables; }); }, })) ); -export const selectVariables = (state: VariableState) => state.variables; +export const selectCollectionVariables = (state: VariableState) => state.collectionVariables; +export const selectEnvironmentVariables = (state: VariableState) => state.environmentVariables; export const useVariableActions = () => useVariableStore(useActions()); diff --git a/src/shim/event-service.ts b/src/shim/event-service.ts index 5abb84cd..b2bf369b 100644 --- a/src/shim/event-service.ts +++ b/src/shim/event-service.ts @@ -4,6 +4,7 @@ import { Collection } from './objects/collection'; import { TrufosObject } from './objects'; import { VariableMap, VariableObject } from './objects/variables'; import { Folder } from 'shim/objects/folder'; +import { EnvironmentMap } from './objects/environment'; export interface IEventService { /** @@ -71,6 +72,12 @@ export interface IEventService { */ setCollectionVariables(variables: VariableMap): void; + /** + * Replace all existing environment variables with the given ones. + * @param environmentVariables + */ + setEnvironmentVariables(environmentVariables: EnvironmentMap): void; + /** * Select an environment by its key. * @param key The key of the environment to select. diff --git a/src/shim/objects/environment.ts b/src/shim/objects/environment.ts index 97ffbf03..0e76d856 100644 --- a/src/shim/objects/environment.ts +++ b/src/shim/objects/environment.ts @@ -8,3 +8,5 @@ export type EnvironmentObject = { /** Variables that exist in the environment */ variables: VariableMap; }; + +export type EnvironmentObjectWithKey = EnvironmentObject & { key: string }; diff --git a/src/shim/objects/variables.ts b/src/shim/objects/variables.ts index 434e76b6..9405b867 100644 --- a/src/shim/objects/variables.ts +++ b/src/shim/objects/variables.ts @@ -12,3 +12,5 @@ export type VariableObject = { /** A description of the variable. */ description?: string; }; + +export type VariableObjectWithKey = VariableObject & { key: string };