diff --git a/package.json b/package.json index bdb562ac4..102b9ece4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "untitled-app", - "version": "0.0.4", + "version": "0.0.3", "private": true, "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.4.2", @@ -33,7 +33,6 @@ "react-router-dom": "^6.14.2", "sketch-helpers": "^0.0.4", "swr": "^2.0.4", - "tauri-plugin-fs-extra-api": "https://github.com/tauri-apps/tauri-plugin-fs-extra#v1", "toml": "^3.0.0", "ts-node": "^10.9.1", "typescript": "^4.4.2", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index ac81a9e97..c485d137b 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -81,7 +81,6 @@ dependencies = [ "serde_json", "tauri", "tauri-build", - "tauri-plugin-fs-extra", "tokio", "toml 0.6.0", ] @@ -3131,18 +3130,6 @@ dependencies = [ "tauri-utils", ] -[[package]] -name = "tauri-plugin-fs-extra" -version = "0.0.0" -source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#7e58dc8502f654b99d51c087421f84ccc0e03119" -dependencies = [ - "log", - "serde", - "serde_json", - "tauri", - "thiserror", -] - [[package]] name = "tauri-runtime" version = "0.13.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 364786f91..35b020801 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -19,10 +19,9 @@ anyhow = "1" oauth2 = "4.4.1" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -tauri = { version = "1.3.0", features = [ "updater", "path-all", "dialog-all", "fs-all", "http-request", "shell-open", "shell-open-api"] } +tauri = { version = "1.3.0", features = [ "updater", "dialog-all", "fs-all", "http-request", "shell-open", "shell-open-api"] } tokio = { version = "1.29.1", features = ["time"] } toml = "0.6.0" -tauri-plugin-fs-extra = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } [features] # this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled. diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 760b302a6..f7581221e 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -98,7 +98,6 @@ fn main() { Ok(()) }) .invoke_handler(tauri::generate_handler![login, read_toml, read_txt_file]) - .plugin(tauri_plugin_fs_extra::init()) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 83235b0e9..4e6b8e2c6 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -8,7 +8,7 @@ }, "package": { "productName": "KittyCAD Modeling", - "version": "0.0.4" + "version": "0.0.3" }, "tauri": { "allowlist": { @@ -38,9 +38,6 @@ }, "shell": { "open": true - }, - "path": { - "all": true } }, "bundle": { diff --git a/src/App.test.tsx b/src/App.test.tsx index 9548e99b7..324db079e 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -1,6 +1,5 @@ import { render, screen } from '@testing-library/react' import { App } from './App' -import { describe, test, vi } from 'vitest' import { BrowserRouter } from 'react-router-dom' let listener: ((rect: any) => void) | undefined = undefined @@ -13,27 +12,12 @@ let listener: ((rect: any) => void) | undefined = undefined disconnect() {} } -describe('App tests', () => { - test('Renders the modeling app screen, including "Variables" pane.', () => { - vi.mock('react-router-dom', async () => { - const actual = (await vi.importActual('react-router-dom')) as Record< - string, - any - > - return { - ...actual, - useParams: () => ({ id: 'new' }), - useLoaderData: () => ({ code: null }), - } - }) - render( - - - - ) - const linkElement = screen.getByText(/Variables/i) - expect(linkElement).toBeInTheDocument() - - vi.restoreAllMocks() - }) +test('renders learn react link', () => { + render( + + + + ) + const linkElement = screen.getByText(/Variables/i) + expect(linkElement).toBeInTheDocument() }) diff --git a/src/App.tsx b/src/App.tsx index 1ec9a9679..5535bbdcd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -43,16 +43,8 @@ import { useHotkeys } from 'react-hotkeys-hook' import { TEST } from './env' import { getNormalisedCoordinates } from './lib/utils' import { getSystemTheme } from './lib/getSystemTheme' -import { isTauri } from './lib/isTauri' -import { useLoaderData, useParams } from 'react-router-dom' -import { writeTextFile } from '@tauri-apps/api/fs' -import { FILE_EXT, PROJECT_ENTRYPOINT } from './lib/tauriFS' -import { IndexLoaderData } from './Router' -import { toast } from 'react-hot-toast' export function App() { - const { code: loadedCode } = useLoaderData() as IndexLoaderData - const pathParams = useParams() const streamRef = useRef(null) useHotKeyListener() const { @@ -159,34 +151,9 @@ export function App() { ? 'opacity-40' : '' - // Use file code loaded from disk - // on mount, and overwrite any locally-stored code - useEffect(() => { - if (isTauri() && loadedCode !== null) { - setCode(loadedCode) - } - return () => { - // Clear code on unmount if in desktop app - if (isTauri()) { - setCode('') - } - } - }, [loadedCode, setCode]) - // const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => { const onChange = (value: string, viewUpdate: ViewUpdate) => { setCode(value) - if (isTauri() && pathParams.id) { - // Save the file to disk - // Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files - writeTextFile(pathParams.id + '/' + PROJECT_ENTRYPOINT, value).catch( - (err) => { - // TODO: add Sentry per GH issue #254 (https://github.com/KittyCAD/modeling-app/issues/254) - console.error('error saving file', err) - toast.error('Error saving file, please check file permissions') - } - ) - } if (editorView) { editorView?.dispatch({ effects: addLineHighlight.of([0, 0]) }) } @@ -446,11 +413,6 @@ export function App() { paneOpacity + (isMouseDownInStream ? ' pointer-events-none' : '') } - filename={ - pathParams.id - ?.slice(pathParams.id.lastIndexOf('/') + 1) - .replace(FILE_EXT, '') || '' - } /> ) => (prepend: string) => { @@ -36,34 +26,16 @@ const prependRoutes = export const paths = { INDEX: '/', - HOME: '/home', - FILE: '/file', SETTINGS: '/settings', SIGN_IN: '/signin', ONBOARDING: prependRoutes(onboardingPaths)( - '/onboarding' + '/onboarding/' ) as typeof onboardingPaths, } -export type IndexLoaderData = { - code: string | null -} - -export type ProjectWithEntryPointMetadata = FileEntry & { - entrypoint_metadata: Metadata -} -export type HomeLoaderData = { - projects: ProjectWithEntryPointMetadata[] -} - const router = createBrowserRouter([ { path: paths.INDEX, - loader: () => - isTauri() ? redirect(paths.HOME) : redirect(paths.FILE + '/new'), - }, - { - path: paths.FILE + '/:id', element: ( @@ -71,10 +43,7 @@ const router = createBrowserRouter([ ), errorElement: , - loader: async ({ - request, - params, - }): Promise => { + loader: ({ request }) => { const store = localStorage.getItem('store') if (store === null) { return redirect(paths.ONBOARDING.INDEX) @@ -91,72 +60,23 @@ const router = createBrowserRouter([ notEnRouteToOnboarding && hasValidOnboardingStatus if (shouldRedirectToOnboarding) { - return redirect(makeUrlPathRelative(paths.ONBOARDING.INDEX) + status) + return redirect(paths.ONBOARDING.INDEX + status) } } - - if (params.id && params.id !== 'new') { - // Note that PROJECT_ENTRYPOINT is hardcoded until we support multiple files - const code = await readTextFile(params.id + '/' + PROJECT_ENTRYPOINT) - - return { - code, - } - } - - return { - code: '', - } + return null }, children: [ { - path: makeUrlPathRelative(paths.SETTINGS), + path: paths.SETTINGS, element: , }, { - path: makeUrlPathRelative(paths.ONBOARDING.INDEX), + path: paths.ONBOARDING.INDEX, element: , children: onboardingRoutes, }, ], }, - { - path: paths.HOME, - element: ( - - - - - ), - loader: async () => { - if (!isTauri()) { - return redirect(paths.FILE + '/new') - } - - const projectDir = await initializeProjectDirectory() - const projectsNoMeta = (await readDir(projectDir.dir)).filter( - isProjectDirectory - ) - const projects = await Promise.all( - projectsNoMeta.map(async (p) => ({ - entrypoint_metadata: await metadata( - p.path + '/' + PROJECT_ENTRYPOINT - ), - ...p, - })) - ) - - return { - projects, - } - }, - children: [ - { - path: makeUrlPathRelative(paths.SETTINGS), - element: , - }, - ], - }, { path: paths.SIGN_IN, element: , diff --git a/src/components/ActionButton.tsx b/src/components/ActionButton.tsx index be0dc45f4..e1f0f8eb2 100644 --- a/src/components/ActionButton.tsx +++ b/src/components/ActionButton.tsx @@ -1,92 +1,52 @@ +import { Link } from 'react-router-dom' import { ActionIcon, ActionIconProps } from './ActionIcon' import React from 'react' import { paths } from '../Router' -import { Link } from 'react-router-dom' -import type { LinkProps } from 'react-router-dom' -interface BaseActionButtonProps { +interface ActionButtonProps extends React.PropsWithChildren { icon?: ActionIconProps className?: string + onClick?: () => void + to?: string + Element?: + | 'button' + | 'link' + | React.ComponentType> } -type ActionButtonAsButton = BaseActionButtonProps & - Omit< - React.ButtonHTMLAttributes, - keyof BaseActionButtonProps - > & { - Element: 'button' - } - -type ActionButtonAsLink = BaseActionButtonProps & - Omit & { - Element: 'link' - } - -type ActionButtonAsExternal = BaseActionButtonProps & - Omit< - React.AnchorHTMLAttributes, - keyof BaseActionButtonProps - > & { - Element: 'externalLink' - } - -type ActionButtonAsElement = BaseActionButtonProps & - Omit, keyof BaseActionButtonProps> & { - Element: React.ComponentType> - } - -type ActionButtonProps = - | ActionButtonAsButton - | ActionButtonAsLink - | ActionButtonAsExternal - | ActionButtonAsElement - -export const ActionButton = (props: ActionButtonProps) => { +export const ActionButton = ({ + icon, + className, + onClick, + to = paths.INDEX, + Element = 'button', + children, + ...props +}: ActionButtonProps) => { const classNames = `group mono text-base flex items-center gap-2 rounded-sm border border-chalkboard-40 dark:border-chalkboard-60 hover:border-liquid-40 dark:hover:bg-chalkboard-90 p-[3px] text-chalkboard-110 dark:text-chalkboard-10 hover:text-chalkboard-110 hover:dark:text-chalkboard-10 ${ - props.icon ? 'pr-2' : 'px-2' - } ${props.className || ''}` + icon ? 'pr-2' : 'px-2' + } ${className}` - switch (props.Element) { - case 'button': { - // Note we have to destructure 'className' and 'Element' out of props - // because we don't want to pass them to the button element; - // the same is true for the other cases below. - const { Element, icon, children, className, ...rest } = props - return ( - - ) - } - case 'link': { - const { Element, to, icon, children, className, ...rest } = props - return ( - - {icon && } - {children} - - ) - } - case 'externalLink': { - const { Element, icon, children, className, ...rest } = props - return ( - - {icon && } - {children} - - ) - } - default: { - const { Element, icon, children, className, ...rest } = props - if (!Element) throw new Error('Element is required') - - return ( - - {props.icon && } - {children} - - ) - } + if (Element === 'button') { + return ( + + ) + } else if (Element === 'link') { + return ( + + {icon && } + {children} + + ) + } else { + return ( + + {icon && } + {children} + + ) } } diff --git a/src/components/AppHeader.tsx b/src/components/AppHeader.tsx index 85295d5c5..f1174cf01 100644 --- a/src/components/AppHeader.tsx +++ b/src/components/AppHeader.tsx @@ -3,17 +3,14 @@ import { Toolbar } from '../Toolbar' import { useStore } from '../useStore' import UserSidebarMenu from './UserSidebarMenu' import { paths } from '../Router' -import { isTauri } from '../lib/isTauri' interface AppHeaderProps extends React.PropsWithChildren { showToolbar?: boolean - filename?: string className?: string } export const AppHeader = ({ showToolbar = true, - filename = '', children, className = '', }: AppHeaderProps) => { @@ -28,18 +25,13 @@ export const AppHeader = ({ className } > - + KittyCAD App - - {isTauri() && filename ? filename : 'KittyCAD Modeling App'} - + KittyCAD App {/* Toolbar if the context deems it */} {showToolbar && ( diff --git a/src/components/DebugPanel.tsx b/src/components/DebugPanel.tsx index 3cbc2cc7c..3987556d3 100644 --- a/src/components/DebugPanel.tsx +++ b/src/components/DebugPanel.tsx @@ -73,7 +73,6 @@ export const DebugPanel = ({ className, ...props }: CollapsiblePanelProps) => { /> { engineCommandManager?.sendSceneCommand({ type: 'modeling_cmd_req', diff --git a/src/components/ExportButton.tsx b/src/components/ExportButton.tsx index af150b828..2d6a5d2e1 100644 --- a/src/components/ExportButton.tsx +++ b/src/components/ExportButton.tsx @@ -166,7 +166,6 @@ export const ExportButton = () => {
, - f: ProjectWithEntryPointMetadata - ) => Promise - handleDeleteProject: (f: ProjectWithEntryPointMetadata) => Promise -}) { - useHotkeys('esc', () => setIsEditing(false)) - const [isEditing, setIsEditing] = useState(false) - const [isConfirmingDelete, setIsConfirmingDelete] = useState(false) - - function handleSave(e: FormEvent) { - e.preventDefault() - handleRenameProject(e, project).then(() => setIsEditing(false)) - } - - function getDisplayedTime(date: Date) { - const startOfToday = new Date() - startOfToday.setHours(0, 0, 0, 0) - return date.getTime() < startOfToday.getTime() - ? date.toLocaleDateString() - : date.toLocaleTimeString() - } - - return ( -
  • - {isEditing ? ( -
    - -
    - - setIsEditing(false)} - /> -
    -
    - ) : ( - <> -
    - - {project.name?.replace(FILE_EXT, '')} - - - Edited {getDisplayedTime(project.entrypoint_metadata.modifiedAt)} - -
    - setIsEditing(true)} - className="!p-0" - /> - setIsConfirmingDelete(true)} - /> -
    -
    - setIsConfirmingDelete(false)} - className="relative z-50" - > -
    - - - Delete File - - - This will permanently delete "{project.name || 'this file'}". - - -

    - Are you sure you want to delete "{project.name || 'this file'} - "? This action cannot be undone. -

    - -
    - { - await handleDeleteProject(project) - setIsConfirmingDelete(false) - }} - icon={{ - icon: faTrashAlt, - bgClassName: 'bg-destroy-80', - iconClassName: - 'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10 dark:text-destroy-20 dark:group-hover:text-destroy-10 dark:hover:text-destroy-10', - }} - className="hover:border-destroy-40 dark:hover:border-destroy-40" - > - Delete - - setIsConfirmingDelete(false)} - > - Cancel - -
    -
    -
    -
    - - )} -
  • - ) -} - -export default ProjectCard diff --git a/src/components/UserSidebarMenu.test.tsx b/src/components/UserSidebarMenu.test.tsx index 014df2925..176e53e18 100644 --- a/src/components/UserSidebarMenu.test.tsx +++ b/src/components/UserSidebarMenu.test.tsx @@ -3,66 +3,64 @@ import { User } from '../useStore' import UserSidebarMenu from './UserSidebarMenu' import { BrowserRouter } from 'react-router-dom' -describe('UserSidebarMenu tests', () => { - test("Renders user's name and email if available", () => { - const userWellFormed: User = { - id: '8675309', - name: 'Test User', - email: 'kittycad.sidebar.test@example.com', - image: 'https://placekitten.com/200/200', - created_at: 'yesteryear', - updated_at: 'today', - } +it("Renders user's name and email if available", () => { + const userWellFormed: User = { + id: '8675309', + name: 'Test User', + email: 'kittycad.sidebar.test@example.com', + image: 'https://placekitten.com/200/200', + created_at: 'yesteryear', + updated_at: 'today', + } - render( - - - - ) + render( + + + + ) - fireEvent.click(screen.getByTestId('user-sidebar-toggle')) + fireEvent.click(screen.getByTestId('user-sidebar-toggle')) - expect(screen.getByTestId('username')).toHaveTextContent( - userWellFormed.name || '' - ) - expect(screen.getByTestId('email')).toHaveTextContent(userWellFormed.email) - }) - - test("Renders just the user's email if no name is available", () => { - const userNoName: User = { - id: '8675309', - email: 'kittycad.sidebar.test@example.com', - image: 'https://placekitten.com/200/200', - created_at: 'yesteryear', - updated_at: 'today', - } - - render( - - - - ) - - fireEvent.click(screen.getByTestId('user-sidebar-toggle')) - - expect(screen.getByTestId('username')).toHaveTextContent(userNoName.email) - }) - - test('Renders a menu button if no user avatar is available', () => { - const userNoAvatar: User = { - id: '8675309', - name: 'Test User', - email: 'kittycad.sidebar.test@example.com', - created_at: 'yesteryear', - updated_at: 'today', - } - - render( - - - - ) - - expect(screen.getByTestId('user-sidebar-toggle')).toHaveTextContent('Menu') - }) + expect(screen.getByTestId('username')).toHaveTextContent( + userWellFormed.name || '' + ) + expect(screen.getByTestId('email')).toHaveTextContent(userWellFormed.email) +}) + +it("Renders just the user's email if no name is available", () => { + const userNoName: User = { + id: '8675309', + email: 'kittycad.sidebar.test@example.com', + image: 'https://placekitten.com/200/200', + created_at: 'yesteryear', + updated_at: 'today', + } + + render( + + + + ) + + fireEvent.click(screen.getByTestId('user-sidebar-toggle')) + + expect(screen.getByTestId('username')).toHaveTextContent(userNoName.email) +}) + +it('Renders a menu button if no user avatar is available', () => { + const userNoAvatar: User = { + id: '8675309', + name: 'Test User', + email: 'kittycad.sidebar.test@example.com', + created_at: 'yesteryear', + updated_at: 'today', + } + + render( + + + + ) + + expect(screen.getByTestId('user-sidebar-toggle')).toHaveTextContent('Menu') }) diff --git a/src/components/UserSidebarMenu.tsx b/src/components/UserSidebarMenu.tsx index 75c7952a3..4fe258728 100644 --- a/src/components/UserSidebarMenu.tsx +++ b/src/components/UserSidebarMenu.tsx @@ -6,7 +6,6 @@ import { faGithub } from '@fortawesome/free-brands-svg-icons' import { useNavigate } from 'react-router-dom' import { useState } from 'react' import { paths } from '../Router' -import makeUrlPathRelative from '../lib/makeUrlPathRelative' const UserSidebarMenu = ({ user }: { user?: User }) => { const displayedName = getDisplayName(user) @@ -97,14 +96,13 @@ const UserSidebarMenu = ({ user }: { user?: User }) => { )}
    { // since /settings is a nested route the sidebar doesn't close // automatically when navigating to it close() - navigate(makeUrlPathRelative(paths.SETTINGS)) + navigate(paths.SETTINGS) }} > Settings diff --git a/src/index.css b/src/index.css index 87cdc8cd1..f2f8b3ec9 100644 --- a/src/index.css +++ b/src/index.css @@ -32,7 +32,7 @@ body.dark { } ::-webkit-scrollbar { - @apply w-2 h-2 rounded-sm; + @apply w-2 rounded-sm; @apply bg-chalkboard-20; } diff --git a/src/lib/makeUrlPathRelative.ts b/src/lib/makeUrlPathRelative.ts deleted file mode 100644 index 90f04efd6..000000000 --- a/src/lib/makeUrlPathRelative.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default function makeUrlPathRelative(path: string) { - return path.replace(/^\//, '') -} diff --git a/src/lib/tauriFS.test.ts b/src/lib/tauriFS.test.ts deleted file mode 100644 index 60b268686..000000000 --- a/src/lib/tauriFS.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { - MAX_PADDING, - getNextProjectIndex, - interpolateProjectNameWithIndex, -} from './tauriFS' - -describe('Test file utility functions', () => { - it('interpolates a project name without an index', () => { - expect(interpolateProjectNameWithIndex('test', 1)).toBe('test') - }) - - it('interpolates a project name with an index and no padding', () => { - expect(interpolateProjectNameWithIndex('test-$n', 2)).toBe('test-2') - }) - - it('interpolates a project name with an index and padding', () => { - expect(interpolateProjectNameWithIndex('test-$nnn', 12)).toBe('test-012') - }) - - it('interpolates a project name with an index and max padding', () => { - expect(interpolateProjectNameWithIndex('test-$nnnnnnnnnnn', 3)).toBe( - `test-${'0'.repeat(MAX_PADDING)}3` - ) - }) - - const testFiles = [ - { - name: 'new-project-04.kcl', - path: '/projects/new-project-04.kcl', - }, - { - name: 'new-project-007.kcl', - path: '/projects/new-project-007.kcl', - }, - { - name: 'new-project-05.kcl', - path: '/projects/new-project-05.kcl', - }, - { - name: 'new-project-0.kcl', - path: '/projects/new-project-0.kcl', - }, - ] - - it('gets the correct next project index', () => { - expect(getNextProjectIndex('new-project-$n', testFiles)).toBe(8) - }) -}) diff --git a/src/lib/tauriFS.ts b/src/lib/tauriFS.ts deleted file mode 100644 index 2f47d7fee..000000000 --- a/src/lib/tauriFS.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { FileEntry, createDir, exists, writeTextFile } from '@tauri-apps/api/fs' -import { documentDir } from '@tauri-apps/api/path' -import { useStore } from '../useStore' -import { isTauri } from './isTauri' -import { ProjectWithEntryPointMetadata } from '../Router' -import { metadata } from 'tauri-plugin-fs-extra-api' - -const PROJECT_FOLDER = 'kittycad-modeling-projects' -export const FILE_EXT = '.kcl' -export const PROJECT_ENTRYPOINT = 'main' + FILE_EXT -const INDEX_IDENTIFIER = '$n' // $nn.. will pad the number with 0s -export const MAX_PADDING = 7 - -// Initializes the project directory and returns the path -export async function initializeProjectDirectory() { - if (!isTauri()) { - throw new Error( - 'initializeProjectDirectory() can only be called from a Tauri app' - ) - } - const { defaultDir: projectDir, setDefaultDir } = useStore.getState() - - if (projectDir && projectDir.dir.length > 0) { - const dirExists = await exists(projectDir.dir) - if (!dirExists) { - await createDir(projectDir.dir, { recursive: true }) - } - return projectDir - } - - const appData = await documentDir() - - const INITIAL_DEFAULT_DIR = { - dir: appData + PROJECT_FOLDER, - } - - const defaultDirExists = await exists(INITIAL_DEFAULT_DIR.dir) - - if (!defaultDirExists) { - await createDir(INITIAL_DEFAULT_DIR.dir, { recursive: true }) - } - - setDefaultDir(INITIAL_DEFAULT_DIR) - return INITIAL_DEFAULT_DIR -} - -export function isProjectDirectory(fileOrDir: Partial) { - return ( - fileOrDir.children?.length && - fileOrDir.children.some((child) => child.name === PROJECT_ENTRYPOINT) - ) -} - -// Creates a new file in the default directory with the default project name -// Returns the path to the new file -export async function createNewProject( - path: string -): Promise { - if (!isTauri) { - throw new Error('createNewProject() can only be called from a Tauri app') - } - - const dirExists = await exists(path) - if (!dirExists) { - await createDir(path, { recursive: true }).catch((err) => { - console.error('Error creating new directory:', err) - throw err - }) - } - - await writeTextFile(path + '/' + PROJECT_ENTRYPOINT, '').catch((err) => { - console.error('Error creating new file:', err) - throw err - }) - - const m = await metadata(path) - - return { - name: path.slice(path.lastIndexOf('/') + 1), - path: path, - entrypoint_metadata: m, - children: [ - { - name: PROJECT_ENTRYPOINT, - path: path + '/' + PROJECT_ENTRYPOINT, - children: [], - }, - ], - } -} - -// create a regex to match the project name -// replacing any instances of "$n" with a regex to match any number -function interpolateProjectName(projectName: string) { - const regex = new RegExp( - projectName.replace(getPaddedIdentifierRegExp(), '([0-9]+)') - ) - return regex -} - -// Returns the next available index for a project name -export function getNextProjectIndex(projectName: string, files: FileEntry[]) { - const regex = interpolateProjectName(projectName) - const matches = files.map((file) => file.name?.match(regex)) - const indices = matches - .filter(Boolean) - .map((match) => match![1]) - .map(Number) - const maxIndex = Math.max(...indices, -1) - return maxIndex + 1 -} - -// Interpolates the project name with the next available index, -// padding the index with 0s if necessary -export function interpolateProjectNameWithIndex( - projectName: string, - index: number -) { - const regex = getPaddedIdentifierRegExp() - - const matches = projectName.match(regex) - const padStartLength = Math.min( - matches !== null ? matches[1]?.length || 0 : 0, - MAX_PADDING - ) - return projectName.replace( - regex, - index.toString().padStart(padStartLength + 1, '0') - ) -} - -export function doesProjectNameNeedInterpolated(projectName: string) { - return projectName.includes(INDEX_IDENTIFIER) -} - -function escapeRegExpChars(string: string) { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') -} - -function getPaddedIdentifierRegExp() { - const escapedIdentifier = escapeRegExpChars(INDEX_IDENTIFIER) - return new RegExp(`${escapedIdentifier}(${escapedIdentifier.slice(-1)}*)`) -} diff --git a/src/routes/Home.tsx b/src/routes/Home.tsx deleted file mode 100644 index 11146fb60..000000000 --- a/src/routes/Home.tsx +++ /dev/null @@ -1,258 +0,0 @@ -import { FormEvent, useCallback, useEffect, useState } from 'react' -import { readDir, removeDir, renameFile } from '@tauri-apps/api/fs' -import { - createNewProject, - getNextProjectIndex, - interpolateProjectNameWithIndex, - doesProjectNameNeedInterpolated, - isProjectDirectory, - PROJECT_ENTRYPOINT, -} from '../lib/tauriFS' -import { ActionButton } from '../components/ActionButton' -import { - faArrowDown, - faArrowUp, - faCircleDot, - faPlus, -} from '@fortawesome/free-solid-svg-icons' -import { useStore } from '../useStore' -import { toast } from 'react-hot-toast' -import { AppHeader } from '../components/AppHeader' -import ProjectCard from '../components/ProjectCard' -import { useLoaderData, useSearchParams } from 'react-router-dom' -import { Link } from 'react-router-dom' -import { ProjectWithEntryPointMetadata, HomeLoaderData } from '../Router' -import Loading from '../components/Loading' -import { metadata } from 'tauri-plugin-fs-extra-api' - -const DESC = ':desc' - -// This route only opens in the Tauri desktop context for now, -// as defined in Router.tsx, so we can use the Tauri APIs and types. -const Home = () => { - const [searchParams, setSearchParams] = useSearchParams() - const sort = searchParams.get('sort_by') ?? 'modified:desc' - const { projects: loadedProjects } = useLoaderData() as HomeLoaderData - const [isLoading, setIsLoading] = useState(true) - const [projects, setProjects] = useState(loadedProjects || []) - const { defaultDir, defaultProjectName } = useStore((s) => ({ - defaultDir: s.defaultDir, - defaultProjectName: s.defaultProjectName, - })) - - const refreshProjects = useCallback( - async (projectDir = defaultDir) => { - const readProjects = ( - await readDir(projectDir.dir, { - recursive: true, - }) - ).filter(isProjectDirectory) - - const projectsWithMetadata = await Promise.all( - readProjects.map(async (p) => ({ - entrypoint_metadata: await metadata( - p.path + '/' + PROJECT_ENTRYPOINT - ), - ...p, - })) - ) - - setProjects(projectsWithMetadata) - }, - [defaultDir, setProjects] - ) - - useEffect(() => { - refreshProjects(defaultDir).then(() => { - setIsLoading(false) - }) - }, [setIsLoading, refreshProjects, defaultDir]) - - async function handleNewProject() { - let projectName = defaultProjectName - if (doesProjectNameNeedInterpolated(projectName)) { - const nextIndex = await getNextProjectIndex(defaultProjectName, projects) - projectName = interpolateProjectNameWithIndex( - defaultProjectName, - nextIndex - ) - } - - await createNewProject(defaultDir.dir + '/' + projectName).catch((err) => { - console.error('Error creating project:', err) - toast.error('Error creating project') - }) - - await refreshProjects() - toast.success('Project created') - } - - async function handleRenameProject( - e: FormEvent, - project: ProjectWithEntryPointMetadata - ) { - const { newProjectName } = Object.fromEntries( - new FormData(e.target as HTMLFormElement) - ) - if (newProjectName && project.name && newProjectName !== project.name) { - const dir = project.path?.slice(0, project.path?.lastIndexOf('/')) - await renameFile(project.path, dir + '/' + newProjectName).catch( - (err) => { - console.error('Error renaming project:', err) - toast.error('Error renaming project') - } - ) - - await refreshProjects() - toast.success('Project renamed') - } - } - - async function handleDeleteProject(project: ProjectWithEntryPointMetadata) { - if (project.path) { - await removeDir(project.path, { recursive: true }).catch((err) => { - console.error('Error deleting project:', err) - toast.error('Error deleting project') - }) - - await refreshProjects() - toast.success('Project deleted') - } - } - - function getSortIcon(sortBy: string) { - if (sort === sortBy) { - return faArrowUp - } else if (sort === sortBy + DESC) { - return faArrowDown - } - return faCircleDot - } - - function getNextSearchParams(sortBy: string) { - if (sort === null || !sort) - return { sort_by: sortBy + (sortBy !== 'modified' ? DESC : '') } - if (sort.includes(sortBy) && !sort.includes(DESC)) return { sort_by: '' } - return { - sort_by: sortBy + (sort.includes(DESC) ? '' : DESC), - } - } - - function getSortFunction(sortBy: string) { - const sortByName = ( - a: ProjectWithEntryPointMetadata, - b: ProjectWithEntryPointMetadata - ) => { - if (a.name && b.name) { - return sortBy.includes('desc') - ? a.name.localeCompare(b.name) - : b.name.localeCompare(a.name) - } - return 0 - } - - const sortByModified = ( - a: ProjectWithEntryPointMetadata, - b: ProjectWithEntryPointMetadata - ) => { - if ( - a.entrypoint_metadata?.modifiedAt && - b.entrypoint_metadata?.modifiedAt - ) { - return !sortBy || sortBy.includes('desc') - ? b.entrypoint_metadata.modifiedAt.getTime() - - a.entrypoint_metadata.modifiedAt.getTime() - : a.entrypoint_metadata.modifiedAt.getTime() - - b.entrypoint_metadata.modifiedAt.getTime() - } - return 0 - } - - if (sortBy?.includes('name')) { - return sortByName - } else { - return sortByModified - } - } - - return ( -
    - -
    -
    -

    Your Projects

    -
    - setSearchParams(getNextSearchParams('name'))} - icon={{ - icon: getSortIcon('name'), - bgClassName: !sort?.includes('name') - ? 'bg-liquid-30 dark:bg-liquid-70' - : '', - }} - > - Name - - setSearchParams(getNextSearchParams('modified'))} - icon={{ - icon: sort ? getSortIcon('modified') : faArrowDown, - bgClassName: !( - sort?.includes('modified') || - !sort || - sort === null - ) - ? 'bg-liquid-30 dark:bg-liquid-70' - : '', - }} - > - Last Modified - -
    -
    -
    -

    - Are being saved at{' '} - - {defaultDir.dir} - - , which you can change in your Settings. -

    - {isLoading ? ( - Loading your Projects... - ) : ( - <> - {projects.length > 0 ? ( -
      - {projects.sort(getSortFunction(sort)).map((project) => ( - - ))} -
    - ) : ( -

    - No Projects found, ready to make your first one? -

    - )} - - New file - - - )} -
    -
    -
    - ) -} - -export default Home diff --git a/src/routes/Onboarding/Camera.tsx b/src/routes/Onboarding/Camera.tsx index 39a8ecdd4..0f4ae0cb1 100644 --- a/src/routes/Onboarding/Camera.tsx +++ b/src/routes/Onboarding/Camera.tsx @@ -1,14 +1,14 @@ import { faArrowRight, faXmark } from '@fortawesome/free-solid-svg-icons' import { ActionButton } from '../../components/ActionButton' -import { onboardingPaths, useDismiss, useNextClick } from '.' +import { useDismiss, useNextClick } from '.' import { useStore } from '../../useStore' -export default function Units() { +const Units = () => { const { isMouseDownInStream } = useStore((s) => ({ isMouseDownInStream: s.isMouseDownInStream, })) const dismiss = useDismiss() - const next = useNextClick(onboardingPaths.SKETCHING) + const next = useNextClick('sketching') return (
    @@ -26,7 +26,6 @@ export default function Units() {

    Dismiss - + Next: Sketching
    @@ -50,3 +45,5 @@ export default function Units() {
    ) } + +export default Units diff --git a/src/routes/Onboarding/Introduction.tsx b/src/routes/Onboarding/Introduction.tsx index c0fc20339..948214f5c 100644 --- a/src/routes/Onboarding/Introduction.tsx +++ b/src/routes/Onboarding/Introduction.tsx @@ -1,10 +1,10 @@ import { faArrowRight, faXmark } from '@fortawesome/free-solid-svg-icons' import { ActionButton } from '../../components/ActionButton' -import { onboardingPaths, useDismiss, useNextClick } from '.' +import { useDismiss, useNextClick } from '.' -export default function Introduction() { +const Introduction = () => { const dismiss = useDismiss() - const next = useNextClick(onboardingPaths.UNITS) + const next = useNextClick('units') return (
    @@ -22,7 +22,6 @@ export default function Introduction() {

    Dismiss - + Get Started
    @@ -46,3 +41,5 @@ export default function Introduction() {
    ) } + +export default Introduction diff --git a/src/routes/Onboarding/Sketching.tsx b/src/routes/Onboarding/Sketching.tsx index 3c33357ff..fbf8a5b79 100644 --- a/src/routes/Onboarding/Sketching.tsx +++ b/src/routes/Onboarding/Sketching.tsx @@ -2,7 +2,7 @@ import { faArrowRight, faXmark } from '@fortawesome/free-solid-svg-icons' import { ActionButton } from '../../components/ActionButton' import { useDismiss } from '.' -export default function Sketching() { +const Sketching = () => { const dismiss = useDismiss() return ( @@ -14,7 +14,6 @@ export default function Sketching() {

    Dismiss - + Finish
    @@ -38,3 +33,5 @@ export default function Sketching() {
    ) } + +export default Sketching diff --git a/src/routes/Onboarding/Units.tsx b/src/routes/Onboarding/Units.tsx index e659362c5..c0e20eb9d 100644 --- a/src/routes/Onboarding/Units.tsx +++ b/src/routes/Onboarding/Units.tsx @@ -4,11 +4,11 @@ import { ActionButton } from '../../components/ActionButton' import { SettingsSection } from '../Settings' import { Toggle } from '../../components/Toggle/Toggle' import { useState } from 'react' -import { onboardingPaths, useDismiss, useNextClick } from '.' +import { useDismiss, useNextClick } from '.' -export default function Units() { +const Units = () => { const dismiss = useDismiss() - const next = useNextClick(onboardingPaths.CAMERA) + const next = useNextClick('camera') const { defaultUnitSystem: ogDefaultUnitSystem, setDefaultUnitSystem: saveDefaultUnitSystem, @@ -67,7 +67,6 @@ export default function Units() {
    Dismiss - + Next: Camera
    @@ -91,3 +86,5 @@ export default function Units() {
    ) } + +export default Units diff --git a/src/routes/Onboarding/index.tsx b/src/routes/Onboarding/index.tsx index 14996d4d9..fa5f73a37 100644 --- a/src/routes/Onboarding/index.tsx +++ b/src/routes/Onboarding/index.tsx @@ -8,13 +8,12 @@ import Camera from './Camera' import Sketching from './Sketching' import { useCallback } from 'react' import { paths } from '../../Router' -import makeUrlPathRelative from '../../lib/makeUrlPathRelative' export const onboardingPaths = { - INDEX: '/', - UNITS: '/units', - CAMERA: '/camera', - SKETCHING: '/sketching', + INDEX: '', + UNITS: 'units', + CAMERA: 'camera', + SKETCHING: 'sketching', } export const onboardingRoutes = [ @@ -23,15 +22,15 @@ export const onboardingRoutes = [ element: , }, { - path: makeUrlPathRelative(onboardingPaths.UNITS), + path: onboardingPaths.UNITS, element: , }, { - path: makeUrlPathRelative(onboardingPaths.CAMERA), + path: onboardingPaths.CAMERA, element: , }, { - path: makeUrlPathRelative(onboardingPaths.SKETCHING), + path: onboardingPaths.SKETCHING, element: , }, ] @@ -44,7 +43,7 @@ export function useNextClick(newStatus: string) { return useCallback(() => { setOnboardingStatus(newStatus) - navigate((newStatus !== onboardingPaths.UNITS ? '..' : '.') + newStatus) + navigate('/onboarding/' + newStatus) }, [newStatus, setOnboardingStatus, navigate]) } diff --git a/src/routes/Settings.tsx b/src/routes/Settings.tsx index 8d187b3fa..b828aafa4 100644 --- a/src/routes/Settings.tsx +++ b/src/routes/Settings.tsx @@ -16,7 +16,7 @@ import { paths } from '../Router' export const Settings = () => { const navigate = useNavigate() - useHotkeys('esc', () => navigate('../')) + useHotkeys('esc', () => navigate(paths.INDEX)) const { defaultDir, setDefaultDir, @@ -46,7 +46,6 @@ export const Settings = () => { theme: s.theme, setTheme: s.setTheme, })) - const ogDefaultDir = useRef(defaultDir) const ogDefaultProjectName = useRef(defaultProjectName) async function handleDirectorySelection() { @@ -66,7 +65,7 @@ export const Settings = () => { { base: defaultDir.base, dir: e.target.value, }) - }} - onBlur={() => { - ogDefaultDir.current.dir !== defaultDir.dir && - toast.success('Default directory updated') - ogDefaultDir.current.dir = defaultDir.dir + toast.success('Default directory updated') }} /> { onBlur={() => { ogDefaultProjectName.current !== defaultProjectName && toast.success('Default project name updated') - ogDefaultProjectName.current = defaultProjectName }} /> @@ -228,10 +222,9 @@ export const Settings = () => { description="Replay the onboarding process" > { setOnboardingStatus('') - navigate('..' + paths.ONBOARDING.INDEX) + navigate(paths.ONBOARDING.INDEX) }} icon={{ icon: faArrowRotateBack }} > diff --git a/src/routes/SignIn.tsx b/src/routes/SignIn.tsx index de78b346f..289ecc60c 100644 --- a/src/routes/SignIn.tsx +++ b/src/routes/SignIn.tsx @@ -61,7 +61,6 @@ const SignIn = () => {

    {isTauri() ? ( ()( // tauri specific app settings defaultDir: { - dir: '', + dir: '~/Documents/', }, setDefaultDir: (dir) => set({ defaultDir: dir }), - defaultProjectName: 'new-project-$nnn', + defaultProjectName: 'new-project-$n', setDefaultProjectName: (defaultProjectName) => set({ defaultProjectName }), defaultUnitSystem: 'imperial', diff --git a/yarn.lock b/yarn.lock index 0331de8eb..136892f28 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1986,7 +1986,7 @@ resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== -"@tauri-apps/api@1.4.0", "@tauri-apps/api@^1.3.0": +"@tauri-apps/api@^1.3.0": version "1.4.0" resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-1.4.0.tgz#b4013ca3d17b853f7df29fe14079ebb4d52dbffa" integrity sha512-Jd6HPoTM1PZSFIzq7FB8VmMu3qSSyo/3lSwLpoapW+lQ41CL5Dow2KryLg+gyazA/58DRWI9vu/XpEeHK4uMdw== @@ -5620,12 +5620,6 @@ tar@^6.1.11: mkdirp "^1.0.3" yallist "^4.0.0" -"tauri-plugin-fs-extra-api@https://github.com/tauri-apps/tauri-plugin-fs-extra#v1": - version "0.0.0" - resolved "https://github.com/tauri-apps/tauri-plugin-fs-extra#1344db48a39b44fe46e9943bf7cddca2fa00caaf" - dependencies: - "@tauri-apps/api" "1.4.0" - test-exclude@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e"