From 2075ed20b314f27fce2b74ea18e24151ff85abda Mon Sep 17 00:00:00 2001 From: Matias Daloia Date: Fri, 28 Feb 2025 10:09:32 +0100 Subject: [PATCH 1/3] feat: add welcome screen --- frontend/src/components/WelcomeScreen.tsx | 105 ++++++++++++++++++++++ frontend/src/components/ui/card.tsx | 76 ++++++++++++++++ frontend/src/hooks/useResults.tsx | 3 +- frontend/src/routes/root.tsx | 24 ++--- frontend/src/style.css | 9 ++ 5 files changed, 206 insertions(+), 11 deletions(-) create mode 100644 frontend/src/components/WelcomeScreen.tsx create mode 100644 frontend/src/components/ui/card.tsx diff --git a/frontend/src/components/WelcomeScreen.tsx b/frontend/src/components/WelcomeScreen.tsx new file mode 100644 index 00000000..402a4325 --- /dev/null +++ b/frontend/src/components/WelcomeScreen.tsx @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: MIT +/* + * Copyright (C) 2018-2025 SCANOSS.COM + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { FolderOpen, Search } from 'lucide-react'; +import { useState } from 'react'; + +import { withErrorHandling } from '@/lib/errors'; +import useConfigStore from '@/stores/useConfigStore'; + +import { SelectDirectory } from '../../wailsjs/go/main/App'; +import { entities } from '../../wailsjs/go/models'; +import { EventsOn } from '../../wailsjs/runtime/runtime'; +import ScanDialog from './ScanDialog'; +import { Button } from './ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from './ui/card'; +import { toast } from './ui/use-toast'; + +export default function WelcomeScreen() { + const setScanRoot = useConfigStore((state) => state.setScanRoot); + const [scanModal, setScanModal] = useState(false); + + const handleSelectScanRoot = withErrorHandling({ + asyncFn: async () => { + const selectedDir = await SelectDirectory(); + if (selectedDir) { + await setScanRoot(selectedDir); + } + }, + onError: () => { + toast({ + variant: 'destructive', + title: 'Error', + description: 'An error occurred while selecting the scan root. Please try again.', + }); + }, + }); + + const handleCloseScanModal = () => { + setScanModal(false); + }; + + const handleShowScanModal = () => { + setScanModal(true); + }; + + EventsOn(entities.Action.ScanWithOptions, () => { + handleShowScanModal(); + }); + + return ( +
+
+

SCANOSS Code Compare

+

Select a folder to start

+
+ +
+ + + Start + + + + + + + + + + Recent + + No recent folders + +
+ + +
+ ); +} diff --git a/frontend/src/components/ui/card.tsx b/frontend/src/components/ui/card.tsx new file mode 100644 index 00000000..cabfbfc5 --- /dev/null +++ b/frontend/src/components/ui/card.tsx @@ -0,0 +1,76 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/frontend/src/hooks/useResults.tsx b/frontend/src/hooks/useResults.tsx index 077e7547..3faebe43 100644 --- a/frontend/src/hooks/useResults.tsx +++ b/frontend/src/hooks/useResults.tsx @@ -26,7 +26,8 @@ export const useResults = () => { }; return { - ...queryResult, + data: queryResult.data, + isLoading: queryResult.isLoading, reset, }; }; diff --git a/frontend/src/routes/root.tsx b/frontend/src/routes/root.tsx index 3b82132c..b625a5e7 100644 --- a/frontend/src/routes/root.tsx +++ b/frontend/src/routes/root.tsx @@ -21,7 +21,7 @@ * SOFTWARE. */ -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { Outlet } from 'react-router-dom'; import KeyboardShortcutsDialog from '@/components/KeyboardShortcutsDialog'; @@ -29,11 +29,14 @@ import ScanDialog from '@/components/ScanDialog'; import Sidebar from '@/components/Sidebar'; import StatusBar from '@/components/StatusBar'; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable'; +import WelcomeScreen from '@/components/WelcomeScreen'; +import useConfigStore from '@/stores/useConfigStore'; import { entities } from '../../wailsjs/go/models'; import { EventsOn } from '../../wailsjs/runtime/runtime'; export default function Root() { + const scanRoot = useConfigStore((state) => state.scanRoot); const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState(false); const [scanModal, setScanModal] = useState(false); const handleCloseScanModal = () => { @@ -44,15 +47,16 @@ export default function Root() { setScanModal(true); }; - useEffect(() => { - // Register event listeners - EventsOn(entities.Action.ShowKeyboardShortcutsModal, () => { - setShowKeyboardShortcuts(true); - }); - EventsOn(entities.Action.ScanWithOptions, () => { - handleShowScanModal(); - }); - }, []); + EventsOn(entities.Action.ShowKeyboardShortcutsModal, () => { + setShowKeyboardShortcuts(true); + }); + EventsOn(entities.Action.ScanWithOptions, () => { + handleShowScanModal(); + }); + + if (scanRoot === '/') { + return ; + } return (
diff --git a/frontend/src/style.css b/frontend/src/style.css index 6e5439f2..f4ee5894 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -93,3 +93,12 @@ body { width: 3px !important; margin-left: 5px; } + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} From 1995815e4766e555a3954a850a73a6b34cd7e570 Mon Sep 17 00:00:00 2001 From: Matias Daloia Date: Fri, 28 Feb 2025 10:34:52 +0100 Subject: [PATCH 2/3] feat: get recent scan roots --- app.go | 4 ++ frontend/src/components/WelcomeScreen.tsx | 41 +++++++++++++--- frontend/src/stores/useConfigStore.ts | 6 ++- frontend/wailsjs/go/main/App.d.ts | 2 + frontend/wailsjs/go/main/App.js | 4 ++ internal/config/config.go | 59 ++++++++++++++++++++--- 6 files changed, 102 insertions(+), 14 deletions(-) diff --git a/app.go b/app.go index 9f9783be..c5dd2c46 100644 --- a/app.go +++ b/app.go @@ -194,6 +194,10 @@ func (a *App) GetScanRoot() (string, error) { return a.cfg.GetScanRoot(), nil } +func (a *App) GetRecentScanRoots() ([]string, error) { + return a.cfg.GetRecentScanRoots(), nil +} + func (a *App) GetResultFilePath() (string, error) { return a.cfg.GetResultFilePath(), nil } diff --git a/frontend/src/components/WelcomeScreen.tsx b/frontend/src/components/WelcomeScreen.tsx index 402a4325..8bc9fd68 100644 --- a/frontend/src/components/WelcomeScreen.tsx +++ b/frontend/src/components/WelcomeScreen.tsx @@ -37,6 +37,7 @@ import { toast } from './ui/use-toast'; export default function WelcomeScreen() { const setScanRoot = useConfigStore((state) => state.setScanRoot); + const recentScanRoots = useConfigStore((state) => state.recentScanRoots); const [scanModal, setScanModal] = useState(false); const handleSelectScanRoot = withErrorHandling({ @@ -55,13 +56,21 @@ export default function WelcomeScreen() { }, }); - const handleCloseScanModal = () => { - setScanModal(false); - }; + const handleSelectRecentFolder = withErrorHandling({ + asyncFn: async (path: string) => { + await setScanRoot(path); + }, + onError: () => { + toast({ + variant: 'destructive', + title: 'Error', + description: 'An error occurred while selecting the recent folder. Please try again.', + }); + }, + }); - const handleShowScanModal = () => { - setScanModal(true); - }; + const handleCloseScanModal = () => setScanModal(false); + const handleShowScanModal = () => setScanModal(true); EventsOn(entities.Action.ScanWithOptions, () => { handleShowScanModal(); @@ -95,7 +104,25 @@ export default function WelcomeScreen() { Recent - No recent folders + + {recentScanRoots.length > 0 ? ( +
+ {recentScanRoots.map((path) => ( + + ))} +
+ ) : ( +

No recent folders

+ )} +
diff --git a/frontend/src/stores/useConfigStore.ts b/frontend/src/stores/useConfigStore.ts index 7b47c6d8..038ae6a5 100644 --- a/frontend/src/stores/useConfigStore.ts +++ b/frontend/src/stores/useConfigStore.ts @@ -25,6 +25,7 @@ import { create } from 'zustand'; import { devtools } from 'zustand/middleware'; import { + GetRecentScanRoots, GetResultFilePath, GetScanRoot, GetScanSettingsFilePath, @@ -37,6 +38,7 @@ interface ResultsState { scanRoot: string; resultsFile: string; settingsFile: string; + recentScanRoots: string[]; } interface ResultsActions { @@ -54,6 +56,7 @@ export default create()( scanRoot: '', resultsFile: '', settingsFile: '', + recentScanRoots: [], setScanRoot: async (scanRoot: string) => { await SetScanRoot(scanRoot); @@ -72,7 +75,8 @@ export default create()( const scanRoot = await GetScanRoot(); const resultsFile = await GetResultFilePath(); const settingsFile = await GetScanSettingsFilePath(); - set({ scanRoot, resultsFile, settingsFile }); + const recentScanRoots = await GetRecentScanRoots(); + set({ scanRoot, resultsFile, settingsFile, recentScanRoots }); }, })) ); diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts index 86628a2c..7ea7fe79 100755 --- a/frontend/wailsjs/go/main/App.d.ts +++ b/frontend/wailsjs/go/main/App.d.ts @@ -5,6 +5,8 @@ import {service} from '../models'; export function BeforeClose(arg1:context.Context):Promise; +export function GetRecentScanRoots():Promise>; + export function GetResultFilePath():Promise; export function GetScanRoot():Promise; diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js index 5af9f67e..20e8d4dc 100755 --- a/frontend/wailsjs/go/main/App.js +++ b/frontend/wailsjs/go/main/App.js @@ -6,6 +6,10 @@ export function BeforeClose(arg1) { return window['go']['main']['App']['BeforeClose'](arg1); } +export function GetRecentScanRoots() { + return window['go']['main']['App']['GetRecentScanRoots'](); +} + export function GetResultFilePath() { return window['go']['main']['App']['GetResultFilePath'](); } diff --git a/internal/config/config.go b/internal/config/config.go index 5c9ca064..71a0a358 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -32,6 +32,8 @@ import ( "sync" "time" + "slices" + "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/spf13/viper" @@ -59,18 +61,20 @@ type Config struct { resultFilePath string scanRoot string scanSettingsFilePath string + recentScanRoots []string debug bool mu sync.RWMutex listeners []func(*Config) } type ConfigDTO struct { - ApiToken string `json:"apitoken"` - ApiUrl string `json:"apiurl"` - ResultFilePath string `json:"resultfilepath,omitempty"` - ScanRoot string `json:"scanroot,omitempty"` - ScanSettingsFilePath string `json:"scansettingsfilepath,omitempty"` - Debug bool `json:"debug,omitempty"` + ApiToken string `json:"apitoken"` + ApiUrl string `json:"apiurl"` + ResultFilePath string `json:"resultfilepath,omitempty"` + ScanRoot string `json:"scanroot,omitempty"` + ScanSettingsFilePath string `json:"scansettingsfilepath,omitempty"` + RecentScanRoots []string `json:"recentscanroots,omitempty"` + Debug bool `json:"debug,omitempty"` } func (c *Config) MarshalJSON() ([]byte, error) { @@ -80,6 +84,7 @@ func (c *Config) MarshalJSON() ([]byte, error) { ResultFilePath: c.resultFilePath, ScanRoot: c.scanRoot, ScanSettingsFilePath: c.scanSettingsFilePath, + RecentScanRoots: c.recentScanRoots, Debug: c.debug, }) } @@ -94,6 +99,7 @@ func (c *Config) UnmarshalJSON(data []byte) error { c.resultFilePath = j.ResultFilePath c.scanRoot = j.ScanRoot c.scanSettingsFilePath = j.ScanSettingsFilePath + c.recentScanRoots = j.RecentScanRoots c.debug = j.Debug return nil } @@ -170,6 +176,35 @@ func (c *Config) GetDebug() bool { return c.debug } +func (c *Config) GetRecentScanRoots() []string { + c.mu.RLock() + defer c.mu.RUnlock() + return c.recentScanRoots +} + +func (c *Config) AddRecentScanRoot(path string) error { + c.mu.Lock() + defer c.mu.Unlock() + + for i, p := range c.recentScanRoots { + if p == path { + c.recentScanRoots = slices.Delete(c.recentScanRoots, i, i+1) + break + } + } + + c.recentScanRoots = append([]string{path}, c.recentScanRoots...) + + // We show max 10 entries + if len(c.recentScanRoots) > 10 { + c.recentScanRoots = c.recentScanRoots[:10] + } + + viper.Set("recentscanroots", c.recentScanRoots) + + return viper.WriteConfig() +} + func (c *Config) SetApiToken(token string) error { c.mu.Lock() c.apiToken = token @@ -201,6 +236,9 @@ func (c *Config) SetScanRoot(path string) { c.resultFilePath = c.getDefaultResultFilePath(path) c.scanSettingsFilePath = c.getDefaultScanSettingsFilePath(path) c.mu.Unlock() + if err := c.AddRecentScanRoot(path); err != nil { + log.Error().Err(err).Msg("Error adding recent scan root") + } c.notifyListeners() } @@ -218,6 +256,13 @@ func (c *Config) SetDebug(debug bool) { c.notifyListeners() } +func (c *Config) SetRecentScanRoots(roots []string) { + c.mu.Lock() + c.recentScanRoots = roots + c.mu.Unlock() + c.notifyListeners() +} + func (c *Config) GetDefaultConfigFolder() string { homeDir, err := os.UserHomeDir() if err != nil { @@ -310,6 +355,8 @@ func (c *Config) initializeApiConfig(apiKey, apiUrl string) error { } func (c *Config) initializePathConfig(scanRoot, inputFile, scanossSettingsFilePath, originalWorkDir string) error { + c.SetRecentScanRoots(viper.GetStringSlice("recentscanroots")) + if scanRoot != "" { c.SetScanRoot(scanRoot) } From 7b148367f6e711c3ea2e022193fc7f62cd76fa01 Mon Sep 17 00:00:00 2001 From: Matias Daloia Date: Fri, 28 Feb 2025 10:44:59 +0100 Subject: [PATCH 3/3] feat: check specific default path platform dependant --- frontend/src/components/WelcomeScreen.tsx | 14 ++++++++---- frontend/src/lib/utils.ts | 11 ++++++++++ frontend/src/routes/root.tsx | 26 ++++++++++++++++------- 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/WelcomeScreen.tsx b/frontend/src/components/WelcomeScreen.tsx index 8bc9fd68..e0eb7891 100644 --- a/frontend/src/components/WelcomeScreen.tsx +++ b/frontend/src/components/WelcomeScreen.tsx @@ -22,7 +22,7 @@ */ import { FolderOpen, Search } from 'lucide-react'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { withErrorHandling } from '@/lib/errors'; import useConfigStore from '@/stores/useConfigStore'; @@ -72,9 +72,15 @@ export default function WelcomeScreen() { const handleCloseScanModal = () => setScanModal(false); const handleShowScanModal = () => setScanModal(true); - EventsOn(entities.Action.ScanWithOptions, () => { - handleShowScanModal(); - }); + useEffect(() => { + const unsubScanWithOptions = EventsOn(entities.Action.ScanWithOptions, () => { + handleShowScanModal(); + }); + + return () => { + unsubScanWithOptions(); + }; + }, []); return (
diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index 07d87372..ba22e14a 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -48,3 +48,14 @@ export function truncatePath(path: string, maxLength: number = 30) { const last = parts[parts.length - 1]; return `${first}/.../${last}`; } + +export const isDefaultPath = (path: string, platform: string | undefined) => { + switch (platform) { + case 'darwin': + return path === '/' || path === '/Users'; + case 'windows': + return path === 'C:\\' || path === ''; + default: + return path === '/' || path === '/home'; + } +}; diff --git a/frontend/src/routes/root.tsx b/frontend/src/routes/root.tsx index b625a5e7..22f04a40 100644 --- a/frontend/src/routes/root.tsx +++ b/frontend/src/routes/root.tsx @@ -21,7 +21,7 @@ * SOFTWARE. */ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { Outlet } from 'react-router-dom'; import KeyboardShortcutsDialog from '@/components/KeyboardShortcutsDialog'; @@ -30,12 +30,15 @@ import Sidebar from '@/components/Sidebar'; import StatusBar from '@/components/StatusBar'; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable'; import WelcomeScreen from '@/components/WelcomeScreen'; +import useEnvironment from '@/hooks/useEnvironment'; +import { isDefaultPath } from '@/lib/utils'; import useConfigStore from '@/stores/useConfigStore'; import { entities } from '../../wailsjs/go/models'; import { EventsOn } from '../../wailsjs/runtime/runtime'; export default function Root() { + const { environment } = useEnvironment(); const scanRoot = useConfigStore((state) => state.scanRoot); const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState(false); const [scanModal, setScanModal] = useState(false); @@ -47,14 +50,21 @@ export default function Root() { setScanModal(true); }; - EventsOn(entities.Action.ShowKeyboardShortcutsModal, () => { - setShowKeyboardShortcuts(true); - }); - EventsOn(entities.Action.ScanWithOptions, () => { - handleShowScanModal(); - }); + useEffect(() => { + const unsubShowKeyboardShortcuts = EventsOn(entities.Action.ShowKeyboardShortcutsModal, () => { + setShowKeyboardShortcuts(true); + }); + const unsubScanWithOptions = EventsOn(entities.Action.ScanWithOptions, () => { + handleShowScanModal(); + }); - if (scanRoot === '/') { + return () => { + unsubShowKeyboardShortcuts(); + unsubScanWithOptions(); + }; + }, []); + + if (isDefaultPath(scanRoot, environment?.platform)) { return ; }