Skip to content

Commit

Permalink
#118 - add Variables to SettingsModal
Browse files Browse the repository at this point in the history
  • Loading branch information
Michael Giek committed Jan 15, 2025
1 parent e3a80df commit 7f75778
Show file tree
Hide file tree
Showing 11 changed files with 283 additions and 26 deletions.
13 changes: 13 additions & 0 deletions src/main/environment/service/environment-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@ export class EnvironmentService implements Initializable {
}
}

/**
* Sets and saves all variables in the current collection.
* @param variables
*/
public setAndSaveAllVariables(variables: VariableObject[]) {
this.currentCollection.variables = {};
variables.forEach((variable) => (this.currentCollection.variables[variable.key] = variable));
}

/**
* Enables or disables a variable in the current collection.
*
Expand Down Expand Up @@ -96,4 +105,8 @@ export class EnvironmentService implements Initializable {
private getVariableValue(key: string) {
return this.getVariable(key)?.value;
}

public setVariable(variable: VariableObject) {
return (this.currentCollection.variables[variable.key] = variable);
}
}
6 changes: 6 additions & 0 deletions src/main/event/main-event-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { PersistenceService } from '../persistence/service/persistence-service';
import { TrufosObject } from 'shim/objects';
import { EnvironmentService } from 'main/environment/service/environment-service';
import './stream-events';
import { VariableObject } from '../../shim/variables';

const persistenceService = PersistenceService.instance;
const environmentService = EnvironmentService.instance;
Expand Down Expand Up @@ -100,4 +101,9 @@ export class MainEventService implements IEventService {
async getVariable(key: string) {
return environmentService.getVariable(key);
}

async setAndSaveAllVariables(variables: VariableObject[]) {
environmentService.setAndSaveAllVariables(variables);
await persistenceService.saveCollection(environmentService.currentCollection);
}
}
45 changes: 36 additions & 9 deletions src/renderer/components/shared/settings/SettingsModal.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,53 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { FiSettings } from 'react-icons/fi';
import { VariableTab } from '@/components/shared/settings/VariableTab/VariableTab';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import { useSettingsStore } from '@/state/settingsStore';
import { Button } from '@/components/ui/button';
import * as React from 'react';

export const SettingsModal = () => {
const { save, cancel, openModal } = useSettingsStore.getState();
const isOpen = useSettingsStore((state) => state.isOpen);
const allDoubleKeys = useSettingsStore((state) => state.allDoubleKeys);

return (
<Dialog>
<DialogTrigger>
<Dialog open={isOpen} onOpenChange={cancel}>
<DialogTrigger onClick={openModal}>
<FiSettings className="text-xl ml-2" />
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Lorem ipsusm?</DialogTitle>
<DialogDescription>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quos blanditiis tenetur
</DialogDescription>
<DialogContent style={{ minWidth: '100vh' }}>
<DialogHeader className="mt-auto">
<DialogTitle>Settings</DialogTitle>
</DialogHeader>
<Tabs defaultValue="variables" className="h-[calc(50vh)]">
<TabsList>
<TabsTrigger value="variables">Variables</TabsTrigger>
</TabsList>
<TabsContent value="variables" className="max-h-[50vh] overflow-y-auto">
<VariableTab />
</TabsContent>
</Tabs>
<DialogFooter className={'bottom-0'}>
<Button
disabled={allDoubleKeys.length !== 0}
className="mt-0 mr-2 mb-0"
onClick={save}
variant={allDoubleKeys.length === 0 ? 'default' : 'defaultDisable'}
>
<span className="leading-4 font-bold">Save</span>
</Button>
<Button className="mt-0 mr-2 mb-0" onClick={cancel} variant="destructive">
<span className="leading-4 font-bold">Cancel</span>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
Expand Down
116 changes: 116 additions & 0 deletions src/renderer/components/shared/settings/VariableTab/VariableTab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { Divider } from '@/components/shared/Divider';
import { Button } from '@/components/ui/button';
import { AddIcon, CheckedIcon, DeleteIcon } from '@/components/icons';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { cn } from '@/lib/utils';
import { useSettingsStore } from '@/state/settingsStore';

export const VariableTab = () => {
const { addNewVariable, deleteVariable, update, checkDuplicate } = useSettingsStore();
const allVariables = useSettingsStore((state) => state.variables);
const allDoubleKeys = useSettingsStore((state) => state.allDoubleKeys);

return (
<div className="p-4 relative">
<div className="absolute top-4 right-4 left-4 z-10">
<div className="flex">
<Button
className="hover:bg-transparent gap-1 h-fit"
size="sm"
variant="ghost"
onClick={addNewVariable}
>
<AddIcon /> Add Variable
</Button>
</div>
<Divider className="mt-2" />
</div>

<div className="absolute top-16 left-4 bottom-4 right-4">
<Table className="table-auto w-full">
<TableHeader>
<TableRow>
<TableHead className="w-auto">Key</TableHead>
<TableHead className="w-auto">Value</TableHead>
<TableHead className="w-full">Description</TableHead>
<TableHead className="w-16">{/* Action Column */}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{allVariables.map((variable, index) => (
<TableRow key={index}>
<TableCell className="w-1/4 break-all">
<input
type="text"
value={variable.key}
className={`w-full bg-transparent outline-none ${allDoubleKeys.includes(variable.key) ? 'text-danger' : ''}`}
placeholder="Enter variable key"
onChange={(e) => {
update(index, e.target.value, 'key');
checkDuplicate(e.target.value);
}}
/>
</TableCell>
<TableCell className="w-1/4 break-all">
<input
type="text"
value={variable.value}
className="w-full bg-transparent outline-none"
placeholder="Enter variable value"
onChange={(e) => update(index, e.target.value, 'value')}
/>
</TableCell>
<TableCell className="w-full break-all">
<input
type="text"
value={variable.description}
className="w-full bg-transparent outline-none"
placeholder="Enter variable description"
onChange={(e) => update(index, e.target.value, 'description')}
/>
</TableCell>
<TableCell className="w-16 text-right">
<div className="flex items-center justify-center gap-2">
<div className="relative h-4 z-10 cursor-pointer">
<input
type="checkbox"
checked={variable.isActive}
onChange={(e) => update(index, e.target.checked, 'isActive')}
className={cn(
'form-checkbox h-4 w-4 appearance-none border rounded-[2px]',
variable.isActive
? 'border-[rgba(107,194,224,1)] bg-[rgba(25,54,65,1)]'
: 'border-[rgba(238,238,238,1)] bg-transparent'
)}
/>
{variable.isActive && (
<div className="absolute left-0 top-0 h-4 w-4 flex items-center justify-center pointer-events-none rotate-6">
<CheckedIcon size={16} viewBox="0 0 16 16" color="rgba(107,194,224,1)" />
</div>
)}
</div>
<Button
variant="ghost"
size="icon"
className="hover:bg-transparent hover:text-[rgba(107,194,224,1)] active:text-[#12B1E7] h-6 w-6"
onClick={() => deleteVariable(index)}
>
<DeleteIcon />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
);
};
3 changes: 2 additions & 1 deletion src/renderer/components/sidebar/FooterBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';

import { RendererEventService } from '@/services/event/renderer-event-service';
import { GithubIcon } from '@/components/icons';
import { SettingsModal } from '@/components/shared/settings/SettingsModal';

export function FooterBar() {
const [appVersion, setAppVersion] = useState<string>(undefined);
Expand All @@ -25,7 +26,7 @@ export function FooterBar() {
>
<GithubIcon /> {/* Adjust the size as needed */}
</a>
{/*<SettingsModal />*/}
<SettingsModal />
</div>
);
}
1 change: 1 addition & 0 deletions src/renderer/components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const buttonVariants = cva(
variants: {
variant: {
default: 'bg-accent-primary text-accent-tertiary hover:bg-accent-primary/90',
defaultDisable: 'border border-primary text-accent-tertiary hover:bg-accent-primary/90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline: 'border border-primary bg-background hover:bg-accent hover:text-accent-foreground',
secondary:
Expand Down
31 changes: 16 additions & 15 deletions src/renderer/components/ui/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,22 @@ const DialogContent = React.forwardRef<
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
<DialogOverlay>
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogOverlay>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/components/ui/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const Tabs = React.forwardRef<
>(({ className, ...props }, ref) => (
<TabsPrimitive.Root
ref={ref}
className={cn('h-[calc(100vh-112px)] flex flex-col bg-background rounded-md', className)}
className={cn('flex flex-col bg-background rounded-md', className)}
{...props}
/>
));
Expand Down
1 change: 1 addition & 0 deletions src/renderer/services/event/renderer-event-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const METHOD_NAMES = new Set<keyof IEventService>([
'deleteObject',
'getActiveEnvironmentVariables',
'getVariable',
'setAndSaveAllVariables',
]);

const INSTANCE = {} as IEventService;
Expand Down
85 changes: 85 additions & 0 deletions src/renderer/state/settingsStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
import { VariableObject } from 'shim/variables';
import { RendererEventService } from '@/services/event/renderer-event-service';

interface VariableState {
variables: VariableObject[];
collectionId: string;
isOpen: boolean;
allDoubleKeys: string[];
lastInput: string | null;
}

interface VariableStateAction {
openModal: () => void;
addNewVariable: () => void;
deleteVariable: (index: number) => void;
update: (index: number, changeValue: string | boolean, key: keyof VariableObject) => void;
save: () => void;
cancel: () => void;
checkDuplicate: (key: string) => void;
}

export const useSettingsStore = create<VariableState & VariableStateAction>()(
immer((set, get) => ({
variables: [],
collectionId: '',
isOpen: false,
allDoubleKeys: [],
lastInput: null,
openModal: () => {
RendererEventService.instance.loadCollection().then((collection) => {
set({
variables: Object.values(collection.variables),
collectionId: collection.id,
isOpen: true,
});
});
},
addNewVariable: () => {
set((state) => {
state.variables.push({ key: '', value: '', description: '', isActive: false });
});
},
deleteVariable: (index: number) => {
set((state) => {
state.variables.splice(index, 1);
});
},
update: (index: number, changeValue: string | boolean, key: keyof VariableObject) => {
set((state) => {
state.variables[index] = { ...state.variables[index], [key]: changeValue };
});
},
save: () => {
RendererEventService.instance.setAndSaveAllVariables(get().variables);
set((state) => {
state.variables = [];
state.isOpen = false;
state.lastInput = null;
});
},
cancel: () => {
set((state) => {
state.variables = [];
state.isOpen = false;
state.allDoubleKeys = [];
state.lastInput = null;
});
},
checkDuplicate: (key: string) => {
const isDuplicate = get().variables.filter((variable) => variable.key === key).length > 1;
set((state) => {
if (isDuplicate && !state.allDoubleKeys.includes(key)) {
state.allDoubleKeys.push(key);
} else {
state.allDoubleKeys = state.allDoubleKeys.filter(
(doubleKey) => doubleKey !== get().lastInput
);
}
state.lastInput = key;
});
},
}))
);
6 changes: 6 additions & 0 deletions src/shim/event-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,10 @@ export interface IEventService {
* @param key The key of the variable.
*/
getVariable(key: string): Promise<VariableObject>;

/**
* Set a variable.
* @param variables
*/
setAndSaveAllVariables(variables: VariableObject[]): void;
}

0 comments on commit 7f75778

Please sign in to comment.