Most FS functions work now
This commit is contained in:
@ -65,6 +65,7 @@
|
||||
"xstate": "^4.38.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"start:prod": "vite preview --port=3000",
|
||||
"serve": "vite serve --port=3000",
|
||||
"build": "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && source \"$HOME/.cargo/env\" && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -y && yarn build:wasm && vite build",
|
||||
|
@ -15,7 +15,6 @@ import {
|
||||
} from 'xstate'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { fileMachine } from 'machines/fileMachine'
|
||||
import { mkdir, remove, rename, create } from '@tauri-apps/plugin-fs'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { join, sep } from '@tauri-apps/api/path'
|
||||
import { DEFAULT_FILE_NAME, FILE_EXT } from 'lib/constants'
|
||||
@ -87,7 +86,7 @@ export const FileMachineProvider = ({
|
||||
services: {
|
||||
readFiles: async (context: ContextFrom<typeof fileMachine>) => {
|
||||
const newFiles = isDesktop()
|
||||
? (await getProjectInfo(context.project.file.path)).children
|
||||
? (await getProjectInfo(context.project.path)).children
|
||||
: []
|
||||
return {
|
||||
...context.project,
|
||||
@ -99,15 +98,15 @@ export const FileMachineProvider = ({
|
||||
let createdPath: string
|
||||
|
||||
if (event.data.makeDir) {
|
||||
createdPath = await join(context.selectedDirectory.path, createdName)
|
||||
await mkdir(createdPath)
|
||||
createdPath = window.electron.path.join(context.selectedDirectory.path, createdName)
|
||||
await window.electron.mkdir(createdPath)
|
||||
} else {
|
||||
createdPath =
|
||||
context.selectedDirectory.path +
|
||||
window.electron.path.sep +
|
||||
createdName +
|
||||
(createdName.endsWith(FILE_EXT) ? '' : FILE_EXT)
|
||||
await create(createdPath)
|
||||
await window.electron.writeFile(createdPath, '')
|
||||
}
|
||||
|
||||
return {
|
||||
@ -121,14 +120,15 @@ export const FileMachineProvider = ({
|
||||
) => {
|
||||
const { oldName, newName, isDir } = event.data
|
||||
const name = newName ? newName : DEFAULT_FILE_NAME
|
||||
const oldPath = await join(context.selectedDirectory.path, oldName)
|
||||
const newDirPath = await join(context.selectedDirectory.path, name)
|
||||
const oldPath = window.electron.path.join(context.selectedDirectory.path, oldName)
|
||||
const newDirPath = window.electron.path.join(context.selectedDirectory.path, name)
|
||||
const newPath =
|
||||
newDirPath + (name.endsWith(FILE_EXT) || isDir ? '' : FILE_EXT)
|
||||
|
||||
await rename(oldPath, newPath, {})
|
||||
await window.electron.rename(oldPath, newPath, {})
|
||||
|
||||
if (oldPath === file?.path && project?.path) {
|
||||
const currentFilePath = window.electron.path.join(file.path, file.name)
|
||||
if (oldPath === currentFilePath && project?.path) {
|
||||
// If we just renamed the current file, navigate to the new path
|
||||
navigate(paths.FILE + '/' + encodeURIComponent(newPath))
|
||||
} else if (file?.path.includes(oldPath)) {
|
||||
@ -153,11 +153,11 @@ export const FileMachineProvider = ({
|
||||
const isDir = !!event.data.children
|
||||
|
||||
if (isDir) {
|
||||
await remove(event.data.path, {
|
||||
await window.electron.rm(event.data.path, {
|
||||
recursive: true,
|
||||
}).catch((e) => console.error('Error deleting directory', e))
|
||||
} else {
|
||||
await remove(event.data.path).catch((e) =>
|
||||
await window.electron.rm(event.data.path).catch((e) =>
|
||||
console.error('Error deleting file', e)
|
||||
)
|
||||
}
|
||||
@ -169,7 +169,7 @@ export const FileMachineProvider = ({
|
||||
file?.path.includes(event.data.path)) &&
|
||||
project?.path
|
||||
) {
|
||||
navigate(paths.FILE + '/' + encodeURIComponent(project.file.path))
|
||||
navigate(paths.FILE + '/' + encodeURIComponent(project.path))
|
||||
}
|
||||
|
||||
return `Successfully deleted ${isDir ? 'folder' : 'file'} "${
|
||||
|
@ -9,7 +9,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faChevronRight } from '@fortawesome/free-solid-svg-icons'
|
||||
import { useFileContext } from 'hooks/useFileContext'
|
||||
import styles from './FileTree.module.css'
|
||||
import { sortProject } from 'lib/tauriFS'
|
||||
import { sortProject } from 'lib/desktopFS'
|
||||
import { FILE_EXT } from 'lib/constants'
|
||||
import { CustomIcon } from './CustomIcon'
|
||||
import { codeManager, kclManager } from 'lib/singletons'
|
||||
@ -171,7 +171,7 @@ const FileTreeItem = ({
|
||||
// Import non-kcl files
|
||||
// We want to update both the state and editor here.
|
||||
codeManager.updateCodeStateEditor(
|
||||
`import("${fileOrDir.path.replace(project.file.path, '.')}")\n` +
|
||||
`import("${fileOrDir.path.replace(project.path, '.')}")\n` +
|
||||
codeManager.code
|
||||
)
|
||||
codeManager.writeToFile()
|
||||
|
@ -3,7 +3,7 @@ import Tooltip from './Tooltip'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { CustomIcon } from './CustomIcon'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { createAndOpenNewProject } from 'lib/tauriFS'
|
||||
import { createAndOpenNewProject } from 'lib/desktopFS'
|
||||
import { paths } from 'lib/paths'
|
||||
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
||||
import { useLspContext } from './LspProvider'
|
||||
|
@ -36,8 +36,8 @@ function ProjectCard({
|
||||
void handleRenameProject(e, project).then(() => setIsEditing(false))
|
||||
}
|
||||
|
||||
function getDisplayedTime(dateStr: string) {
|
||||
const date = new Date(dateStr)
|
||||
function getDisplayedTime(dateTimeMs: number) {
|
||||
const date = new Date(dateTimeMs)
|
||||
const startOfToday = new Date()
|
||||
startOfToday.setHours(0, 0, 0, 0)
|
||||
return date.getTime() < startOfToday.getTime()
|
||||
@ -103,7 +103,7 @@ function ProjectCard({
|
||||
/>
|
||||
) : (
|
||||
<h3 className="font-sans relative z-0 p-2">
|
||||
{project.file.name?.replace(FILE_EXT, '')}
|
||||
{project.name?.replace(FILE_EXT, '')}
|
||||
</h3>
|
||||
)}
|
||||
<span className="px-2 text-chalkboard-60 text-xs">
|
||||
@ -113,8 +113,8 @@ function ProjectCard({
|
||||
</span>
|
||||
<span className="px-2 text-chalkboard-60 text-xs">
|
||||
Edited{' '}
|
||||
{project.metadata && project.metadata?.modified
|
||||
? getDisplayedTime(project.metadata.modified)
|
||||
{project.metadata && project.metadata.mtimeMs
|
||||
? getDisplayedTime(project.metadata.mtimeMs)
|
||||
: 'never'}
|
||||
</span>
|
||||
</div>
|
||||
@ -169,11 +169,11 @@ function ProjectCard({
|
||||
onDismiss={() => setIsConfirmingDelete(false)}
|
||||
>
|
||||
<p className="my-4">
|
||||
This will permanently delete "{project.file.name || 'this file'}
|
||||
This will permanently delete "{project.name || 'this file'}
|
||||
".
|
||||
</p>
|
||||
<p className="my-4">
|
||||
Are you sure you want to delete "{project.file.name || 'this file'}
|
||||
Are you sure you want to delete "{project.name || 'this file'}
|
||||
"? This action cannot be undone.
|
||||
</p>
|
||||
</DeleteConfirmationDialog>
|
||||
|
@ -24,7 +24,7 @@ export const ProjectCardRenameForm = forwardRef(
|
||||
required
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
defaultValue={project.file.name}
|
||||
defaultValue={project.name}
|
||||
ref={ref}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
|
@ -35,7 +35,7 @@ const ProjectSidebarMenu = ({
|
||||
className="hidden select-none cursor-default text-sm text-chalkboard-110 dark:text-chalkboard-20 whitespace-nowrap lg:block"
|
||||
data-testid="project-name"
|
||||
>
|
||||
{project?.name ? project.file.name : APP_NAME}
|
||||
{project?.name ? project.name : APP_NAME}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@ -212,7 +212,7 @@ function ProjectMenuPopover({
|
||||
</span>
|
||||
{isDesktop() && project?.name && (
|
||||
<span className="hidden text-xs text-chalkboard-70 dark:text-chalkboard-40 whitespace-nowrap lg:block">
|
||||
{project.file.name}
|
||||
{project.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
@ -12,10 +12,10 @@ import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { ActionButton } from 'components/ActionButton'
|
||||
import { SettingsFieldInput } from './SettingsFieldInput'
|
||||
import { getInitialDefaultDir, showInFolder } from 'lib/desktop'
|
||||
import { getInitialDefaultDir } from 'lib/desktop'
|
||||
import toast from 'react-hot-toast'
|
||||
import { APP_VERSION } from 'routes/Settings'
|
||||
import { createAndOpenNewProject, getSettingsFolderPaths } from 'lib/tauriFS'
|
||||
import { createAndOpenNewProject, getSettingsFolderPaths } from 'lib/desktopFS'
|
||||
import { paths } from 'lib/paths'
|
||||
import { useDotDotSlash } from 'hooks/useDotDotSlash'
|
||||
import { sep } from '@tauri-apps/api/path'
|
||||
@ -190,7 +190,7 @@ export const AllSettingsFields = forwardRef(
|
||||
const paths = await getSettingsFolderPaths(
|
||||
projectPath ? decodeURIComponent(projectPath) : undefined
|
||||
)
|
||||
showInFolder(paths[searchParamTab])
|
||||
window.electron.showInFolder(paths[searchParamTab])
|
||||
}}
|
||||
iconStart={{
|
||||
icon: 'folder',
|
||||
|
@ -209,7 +209,7 @@ export const SettingsAuthProviderBase = ({
|
||||
},
|
||||
services: {
|
||||
'Persist settings': (context) =>
|
||||
saveSettings(context, loadedProject?.project?.path),
|
||||
saveSettings(context, loadedProject?.path),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
@ -206,6 +206,7 @@ export const Stream = () => {
|
||||
if (!videoRef.current) return
|
||||
if (!mediaStream) return
|
||||
|
||||
// The browser complains if we try to load a new stream without pausing first.
|
||||
// Do not immediately play the stream!
|
||||
try {
|
||||
videoRef.current.srcObject = mediaStream
|
||||
|
@ -1,12 +1,11 @@
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
||||
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
|
||||
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
||||
import { FileEntry } from 'wasm-lib/kcl/bindings/FileEntry'
|
||||
import { ProjectState } from 'wasm-lib/kcl/bindings/ProjectState'
|
||||
import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute'
|
||||
import { components } from './machine-api'
|
||||
import { isDesktop } from './isDesktop'
|
||||
import { SaveSettingsPayload } from 'lib/settings/settingsUtils'
|
||||
|
||||
import {
|
||||
defaultAppSettings,
|
||||
@ -17,7 +16,6 @@ import {
|
||||
export {
|
||||
parseProjectRoute,
|
||||
} from 'lang/wasm'
|
||||
import { SaveSettingsPayload } from 'lib/settings/settingsUtils'
|
||||
|
||||
const DEFAULT_HOST = 'https://api.zoo.dev'
|
||||
const SETTINGS_FILE_NAME = 'settings.toml'
|
||||
@ -25,7 +23,6 @@ const PROJECT_SETTINGS_FILE_NAME = 'project.toml'
|
||||
const PROJECT_FOLDER = 'zoo-modeling-app-projects'
|
||||
const DEFAULT_PROJECT_KCL_FILE = "main.kcl"
|
||||
|
||||
|
||||
// List machines on the local network.
|
||||
export async function listMachines(): Promise<{
|
||||
[key: string]: components['schemas']['Machine']
|
||||
@ -43,15 +40,32 @@ export async function renameProjectDirectory(
|
||||
projectPath: string,
|
||||
newName: string
|
||||
): Promise<string> {
|
||||
debugger
|
||||
if (!newName) {
|
||||
return Promise.reject(new Error(`New name for project cannot be empty`))
|
||||
}
|
||||
|
||||
export async function showInFolder(path: string | undefined): Promise<void> {
|
||||
if (!path) {
|
||||
console.error('path is undefined cannot call desktop showInFolder')
|
||||
return
|
||||
try { await window.electron.stat(projectPath) }
|
||||
catch (e) {
|
||||
if (e === 'ENOENT') {
|
||||
return Promise.reject(new Error(`Path ${projectPath} is not a directory`))
|
||||
}
|
||||
debugger
|
||||
}
|
||||
|
||||
// Make sure the new name does not exist.
|
||||
const newPath = window.electron.path.join(projectPath.split('/').slice(0, -1).join('/'), newName)
|
||||
try {
|
||||
await window.electron.stat(newPath)
|
||||
// If we get here it means the stat succeeded and there's a file already
|
||||
// with the same name...
|
||||
return Promise.reject(new Error(`Path ${newPath} already exists, cannot rename to an existing path`))
|
||||
} catch (e) {
|
||||
// Otherwise if it failed and the failure is "it doesnt exist" then rename it!
|
||||
if (e === 'ENOENT') {
|
||||
await window.electron.rename(projectPath, newPath)
|
||||
return newPath
|
||||
}
|
||||
}
|
||||
return Promise.reject(new Error('Unreachable'))
|
||||
}
|
||||
|
||||
export async function ensureProjectDirectoryExists(
|
||||
@ -62,7 +76,7 @@ export async function ensureProjectDirectoryExists(
|
||||
await window.electron.stat(projectDir)
|
||||
} catch (e) {
|
||||
if (e === 'ENOENT') {
|
||||
window.electron.mkdir(projectDir, { recursive: true }, (e) => {
|
||||
await window.electron.mkdir(projectDir, { recursive: true }, (e) => {
|
||||
console.log(e)
|
||||
})
|
||||
}
|
||||
@ -92,7 +106,7 @@ export async function createNewProjectDirectory(
|
||||
await window.electron.stat(projectDir)
|
||||
} catch (e) {
|
||||
if (e === 'ENOENT') {
|
||||
window.electron.mkdir(projectDir, { recursive: true })
|
||||
await window.electron.mkdir(projectDir, { recursive: true })
|
||||
}
|
||||
}
|
||||
|
||||
@ -101,17 +115,16 @@ export async function createNewProjectDirectory(
|
||||
DEFAULT_PROJECT_KCL_FILE
|
||||
)
|
||||
await window.electron.writeFile(projectFile, initialCode ?? '')
|
||||
const metadata = await window.electron.stat(projectFile)
|
||||
|
||||
return {
|
||||
file: {
|
||||
path: projectDir,
|
||||
name: projectName,
|
||||
// We don't need to recursively get all files in the project directory.
|
||||
// Because we just created it and it's empty.
|
||||
children: undefined,
|
||||
},
|
||||
default_file: projectFile,
|
||||
metadata: undefined /* TODO */,
|
||||
metadata,
|
||||
kcl_file_count: 1,
|
||||
directory_count: 0,
|
||||
}
|
||||
@ -120,6 +133,9 @@ export async function createNewProjectDirectory(
|
||||
export async function listProjects(
|
||||
configuration?: Partial<SaveSettingsPayload>
|
||||
): Promise<Project[]> {
|
||||
if (configuration === undefined) {
|
||||
configuration = await readAppSettingsFile()
|
||||
}
|
||||
const projectDir = await ensureProjectDirectoryExists(configuration)
|
||||
const projects = []
|
||||
const entries = await window.electron.readdir(projectDir)
|
||||
@ -140,7 +156,7 @@ export async function listProjects(
|
||||
const IMPORT_FILE_EXTENSIONS = [
|
||||
// TODO Use ImportFormat enum
|
||||
"stp", "glb", "fbxb", "kcl"
|
||||
];
|
||||
]
|
||||
|
||||
const isRelevantFile = (filename: string): boolean => IMPORT_FILE_EXTENSIONS.some(
|
||||
(ext) => filename.endsWith('.' + ext))
|
||||
@ -157,13 +173,13 @@ const collectAllFilesRecursiveFrom = async (path: string) => {
|
||||
// Make sure the path is a directory.
|
||||
const isPathDir = await window.electron.statIsDirectory(path)
|
||||
if (!isPathDir) {
|
||||
return Promise.reject(new Error(`Path ${path} is not a directory`));
|
||||
return Promise.reject(new Error(`Path ${path} is not a directory`))
|
||||
}
|
||||
|
||||
const pathParts = path.split('/')
|
||||
let entry = /* FileEntry */ {
|
||||
name: pathParts.slice(-1)[0],
|
||||
path: pathParts.slice(0, -1).join('/'),
|
||||
path,
|
||||
children: [],
|
||||
}
|
||||
|
||||
@ -185,9 +201,9 @@ const collectAllFilesRecursiveFrom = async (path: string) => {
|
||||
if (!isRelevantFile(ePath)) { continue }
|
||||
children.push(/* FileEntry */ {
|
||||
name: e,
|
||||
path: ePath.split('/').slice(0, -1).join('/'),
|
||||
path: ePath,
|
||||
children: undefined,
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -199,13 +215,13 @@ const collectAllFilesRecursiveFrom = async (path: string) => {
|
||||
|
||||
const getDefaultKclFileForDir = async (projectDir, file) => {
|
||||
// Make sure the dir is a directory.
|
||||
const isFileEntryDir = await window.electron.statIsDirectory(file.path)
|
||||
const isFileEntryDir = await window.electron.statIsDirectory(projectDir)
|
||||
if (!isFileEntryDir) {
|
||||
return Promise.reject(new Error(`Path ${file.path} is not a directory`))
|
||||
return Promise.reject(new Error(`Path ${projectDir} is not a directory`))
|
||||
}
|
||||
|
||||
let defaultFilePath = window.electron.path.join(file.path, DEFAULT_PROJECT_KCL_FILE)
|
||||
try { await window.eletron.stat(defaultFilePath) }
|
||||
let defaultFilePath = window.electron.path.join(projectDir, DEFAULT_PROJECT_KCL_FILE)
|
||||
try { await window.electron.stat(defaultFilePath) }
|
||||
catch (e) {
|
||||
if (e === 'ENOENT') {
|
||||
// Find a kcl file in the directory.
|
||||
@ -213,7 +229,7 @@ const getDefaultKclFileForDir = async (projectDir, file) => {
|
||||
for (let entry of file.children) {
|
||||
if (entry.name.endsWith(".kcl")) {
|
||||
return window.electron.path.join(projectDir, entry.name)
|
||||
} else if (entry.children.is_some()) {
|
||||
} else if (entry.children.length > 0) {
|
||||
// Recursively find a kcl file in the directory.
|
||||
return getDefaultKclFileForDir(entry.path, entry)
|
||||
}
|
||||
@ -228,7 +244,7 @@ const getDefaultKclFileForDir = async (projectDir, file) => {
|
||||
}
|
||||
|
||||
const kclFileCount = (file /* fileEntry */) => {
|
||||
let count = 0;
|
||||
let count = 0
|
||||
if (file.children) {
|
||||
for (let entry of file.children) {
|
||||
if (entry.name.endsWith(".kcl")) {
|
||||
@ -263,38 +279,35 @@ export async function getProjectInfo(
|
||||
try { await window.electron.stat(projectPath) }
|
||||
catch (e) {
|
||||
if (e === 'ENOENT') {
|
||||
return Promise.reject(new Error(`Project directory does not exist: ${project_path}`));
|
||||
return Promise.reject(new Error(`Project directory does not exist: ${project_path}`))
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure it is a directory.
|
||||
const projectPathIsDir = await window.electron.statIsDirectory(projectPath)
|
||||
if (!projectPathIsDir) {
|
||||
return Promise.reject(new Error(`Project path is not a directory: ${project_path}`));
|
||||
return Promise.reject(new Error(`Project path is not a directory: ${project_path}`))
|
||||
}
|
||||
|
||||
let walked = await collectAllFilesRecursiveFrom(projectPath)
|
||||
let default_file = await getDefaultKclFileForDir(projectPath, walked)
|
||||
const metadata = await window.electron.stat(projectPath)
|
||||
|
||||
let project = /* FileEntry */ {
|
||||
file: walked,
|
||||
metadata: undefined,
|
||||
...walked,
|
||||
metadata,
|
||||
kcl_file_count: 0,
|
||||
directory_count: 0,
|
||||
default_file,
|
||||
};
|
||||
|
||||
// Populate the number of KCL files in the project.
|
||||
project.kcl_file_count = kclFileCount(project.file)
|
||||
|
||||
//Populate the number of directories in the project.
|
||||
project.directory_count = directoryCount(project.file)
|
||||
|
||||
return project
|
||||
}
|
||||
|
||||
export async function readDirRecursive(path: string): Promise<FileEntry[]> {
|
||||
debugger
|
||||
// Populate the number of KCL files in the project.
|
||||
project.kcl_file_count = kclFileCount(project)
|
||||
|
||||
//Populate the number of directories in the project.
|
||||
project.directory_count = directoryCount(project)
|
||||
|
||||
return project
|
||||
}
|
||||
|
||||
// Write project settings file.
|
||||
@ -320,8 +333,8 @@ const getAppSettingsFilePath = async () => {
|
||||
await window.electron.stat(fullPath)
|
||||
} catch (e) {
|
||||
// File/path doesn't exist
|
||||
if (e.code === 'ENOENT') {
|
||||
window.electron.mkdir(fullPath, { recursive: true })
|
||||
if (e === 'ENOENT') {
|
||||
await window.electron.mkdir(fullPath, { recursive: true })
|
||||
}
|
||||
}
|
||||
return window.electron.path.join(fullPath, SETTINGS_FILE_NAME)
|
||||
@ -331,8 +344,8 @@ const getProjectSettingsFilePath = async (projectPath: string) => {
|
||||
try {
|
||||
await window.electron.stat(projectPath)
|
||||
} catch (e) {
|
||||
if (e.code === 'ENOENT') {
|
||||
window.electron.mkdir(projectPath, { recursive: true })
|
||||
if (e === 'ENOENT') {
|
||||
await window.electron.mkdir(projectPath, { recursive: true })
|
||||
}
|
||||
}
|
||||
return window.electron.path.join(projectPath, PROJECT_SETTINGS_FILE_NAME)
|
||||
@ -419,7 +432,7 @@ export const getUser = async (
|
||||
}
|
||||
|
||||
// Use kittycad library to fetch the user info from /user/me
|
||||
if (baseurl != DEFAULT_HOST) {
|
||||
if (baseurl !== DEFAULT_HOST) {
|
||||
// The TypeScript generated library uses environment variables for this
|
||||
// because it was intended for NodeJS.
|
||||
window.electron.process.env.BASE_URL(baseurl)
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { appConfigDir } from '@tauri-apps/api/path'
|
||||
import { isDesktop } from './isDesktop'
|
||||
import type { FileEntry } from 'lib/types'
|
||||
import {
|
||||
@ -64,9 +63,9 @@ function interpolateProjectName(projectName: string) {
|
||||
}
|
||||
|
||||
// Returns the next available index for a project name
|
||||
export function getNextProjectIndex(projectName: string, files: FileEntry[]) {
|
||||
export function getNextProjectIndex(projectName: string, projects: FileEntry[]) {
|
||||
const regex = interpolateProjectName(projectName)
|
||||
const matches = files.map((file) => file.name?.match(regex))
|
||||
const matches = projects.map((project) => project.name?.match(regex))
|
||||
const indices = matches
|
||||
.filter(Boolean)
|
||||
.map((match) => match![1])
|
||||
@ -108,7 +107,7 @@ function getPaddedIdentifierRegExp() {
|
||||
}
|
||||
|
||||
export async function getSettingsFolderPaths(projectPath?: string) {
|
||||
const user = isDesktop() ? await appConfigDir() : '/'
|
||||
const user = isDesktop() ? await window.electron.getPath('appData') : '/'
|
||||
const project = projectPath !== undefined ? projectPath : undefined
|
||||
|
||||
return {
|
@ -3,7 +3,11 @@ import path from 'path'
|
||||
import fs from 'node:fs/promises'
|
||||
import packageJson from '../../package.json'
|
||||
|
||||
const open = (args: any) => ipcRenderer.invoke('dialog', args)
|
||||
const showInFolder = (path: string) => ipcRenderer.invoke('shell.showItemInFolder', path)
|
||||
|
||||
const readFile = (path: string) => fs.readFile(path, 'utf-8')
|
||||
const rename = (prev: string, next: string) => fs.rename(prev, next)
|
||||
const writeFile = (path: string, data: string) =>
|
||||
fs.writeFile(path, data, 'utf-8')
|
||||
const readdir = (path: string) => fs.readdir(path, 'utf-8')
|
||||
@ -27,13 +31,21 @@ const exposeProcessEnv = (varName: string) => {
|
||||
|
||||
import('@kittycad/lib').then((kittycad) => {
|
||||
contextBridge.exposeInMainWorld('electron', {
|
||||
// Passing fs directly is not recommended since it gives a lot of power
|
||||
// to the browser side / potential malicious code. We restrict what is
|
||||
// exported.
|
||||
readFile,
|
||||
writeFile,
|
||||
readdir,
|
||||
rename,
|
||||
rm: fs.rm,
|
||||
path,
|
||||
stat,
|
||||
statIsDirectory,
|
||||
mkdir: fs.mkdir,
|
||||
// opens a dialog
|
||||
open,
|
||||
showInFolder,
|
||||
getPath,
|
||||
packageJson,
|
||||
platform: process.platform,
|
||||
|
@ -4,7 +4,6 @@ import { isDesktop } from './isDesktop'
|
||||
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
||||
import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute'
|
||||
import { parseProjectRoute, readAppSettingsFile } from './desktop'
|
||||
import { parseProjectRoute as parseProjectRouteWasm } from 'lang/wasm'
|
||||
import { readLocalStorageAppSettingsFile } from './settings/settingsUtils'
|
||||
import { err } from 'lib/trap'
|
||||
|
||||
@ -38,19 +37,17 @@ export async function getProjectMetaByRouteId(
|
||||
): Promise<ProjectRoute | undefined> {
|
||||
if (!id) return undefined
|
||||
|
||||
const inTauri = isDesktop()
|
||||
const onDesktop = isDesktop()
|
||||
|
||||
if (configuration === undefined) {
|
||||
configuration = inTauri
|
||||
configuration = onDesktop
|
||||
? await readAppSettingsFile()
|
||||
: readLocalStorageAppSettingsFile()
|
||||
}
|
||||
|
||||
if (err(configuration)) return Promise.reject(configuration)
|
||||
|
||||
const route = inTauri
|
||||
? await parseProjectRoute(configuration, id)
|
||||
: parseProjectRouteWasm(configuration, id)
|
||||
const route = parseProjectRoute(configuration, id)
|
||||
|
||||
if (err(route)) return Promise.reject(route)
|
||||
|
||||
|
@ -126,7 +126,7 @@ export const fileLoader: LoaderFunction = async ({
|
||||
},
|
||||
file: {
|
||||
name: current_file_name,
|
||||
path: current_file_path,
|
||||
path: current_file_path.split('/').slice(0, -1).join('/'),
|
||||
children: [],
|
||||
},
|
||||
}
|
||||
|
@ -14,7 +14,6 @@ import {
|
||||
} from 'lib/cameraControls'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { useRef } from 'react'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
import { CustomIcon } from 'components/CustomIcon'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
|
||||
@ -209,22 +208,20 @@ export function createSettings() {
|
||||
onClick={async () => {
|
||||
// In Tauri end-to-end tests we can't control the file picker,
|
||||
// so we seed the new directory value in the element's dataset
|
||||
const newValue =
|
||||
inputRef.current && inputRef.current.dataset.testValue
|
||||
? inputRef.current.dataset.testValue
|
||||
: await open({
|
||||
directory: true,
|
||||
recursive: true,
|
||||
const inputRefVal = inputRef.current?.dataset.testValue
|
||||
if (inputRef.current && inputRefVal && !Array.isArray(inputRefVal)) {
|
||||
updateValue(inputRefVal)
|
||||
} else {
|
||||
const newPath = await window.electron.open({
|
||||
properties: [
|
||||
'openDirectory',
|
||||
'createDirectory',
|
||||
],
|
||||
defaultPath: value,
|
||||
title: 'Choose a new project directory',
|
||||
})
|
||||
if (
|
||||
newValue &&
|
||||
newValue !== null &&
|
||||
newValue !== value &&
|
||||
!Array.isArray(newValue)
|
||||
) {
|
||||
updateValue(newValue)
|
||||
if (newPath.canceled) return
|
||||
updateValue(newPath.filePaths[0])
|
||||
}
|
||||
}}
|
||||
className="p-0 m-0 border-none hover:bg-primary/10 focus:bg-primary/10 dark:hover:bg-primary/20 dark:focus::bg-primary/20"
|
||||
|
@ -36,9 +36,9 @@ export function getSortFunction(sortBy: string) {
|
||||
}
|
||||
|
||||
const sortByModified = (a: Project, b: Project) => {
|
||||
if (a.metadata?.modified && b.metadata?.modified) {
|
||||
const aDate = new Date(a.metadata.modified)
|
||||
const bDate = new Date(b.metadata.modified)
|
||||
if (a.metadata?.mtimeMs && b.metadata?.mtimeMs) {
|
||||
const aDate = new Date(a.metadata.mtimeMs)
|
||||
const bDate = new Date(b.metadata.mtimeMs)
|
||||
return !sortBy || sortBy.includes('desc')
|
||||
? bDate.getTime() - aDate.getTime()
|
||||
: aDate.getTime() - bDate.getTime()
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { getNextProjectIndex, interpolateProjectNameWithIndex } from './tauriFS'
|
||||
import { getNextProjectIndex, interpolateProjectNameWithIndex } from './desktopFS'
|
||||
import { MAX_PADDING } from './constants'
|
||||
|
||||
describe('Test project name utility functions', () => {
|
||||
|
11
src/main.ts
11
src/main.ts
@ -2,7 +2,7 @@
|
||||
// template that ElectronJS provides.
|
||||
|
||||
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
||||
import { app, BrowserWindow, ipcMain } from 'electron'
|
||||
import { app, BrowserWindow, ipcMain, dialog, shell } from 'electron'
|
||||
import path from 'path'
|
||||
|
||||
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
|
||||
@ -12,6 +12,7 @@ if (require('electron-squirrel-startup')) {
|
||||
|
||||
const createWindow = () => {
|
||||
let mainWindow = new BrowserWindow({
|
||||
autoHideMenuBar: true,
|
||||
width: 800,
|
||||
height: 600,
|
||||
webPreferences: {
|
||||
@ -52,3 +53,11 @@ app.on('ready', createWindow)
|
||||
ipcMain.handle('app.getPath', (event, data) => {
|
||||
return app.getPath(data)
|
||||
})
|
||||
|
||||
ipcMain.handle('dialog', (event, data) => {
|
||||
return dialog.showOpenDialog(data)
|
||||
})
|
||||
|
||||
ipcMain.handle('shell.showItemInFolder', (event, data) => {
|
||||
return shell.showItemInFolder(data)
|
||||
})
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { FormEvent, useEffect, useRef } from 'react'
|
||||
import { remove } from '@tauri-apps/plugin-fs'
|
||||
import {
|
||||
getNextProjectIndex,
|
||||
interpolateProjectNameWithIndex,
|
||||
doesProjectNameNeedInterpolated,
|
||||
} from 'lib/tauriFS'
|
||||
} from 'lib/desktopFS'
|
||||
import { ActionButton } from 'components/ActionButton'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { AppHeader } from 'components/AppHeader'
|
||||
@ -131,7 +130,7 @@ const Home = () => {
|
||||
}
|
||||
|
||||
await renameProjectDirectory(
|
||||
await join(context.defaultDirectory, oldName),
|
||||
window.electron.path.join(context.defaultDirectory, oldName),
|
||||
name
|
||||
)
|
||||
return `Successfully renamed "${oldName}" to "${name}"`
|
||||
@ -140,7 +139,7 @@ const Home = () => {
|
||||
context: ContextFrom<typeof homeMachine>,
|
||||
event: EventFrom<typeof homeMachine, 'Delete project'>
|
||||
) => {
|
||||
await remove(await join(context.defaultDirectory, event.data.name), {
|
||||
await window.electron.rm(window.electron.path.join(context.defaultDirectory, event.data.name), {
|
||||
recursive: true,
|
||||
})
|
||||
return `Successfully deleted "${event.data.name}"`
|
||||
@ -192,15 +191,15 @@ const Home = () => {
|
||||
new FormData(e.target as HTMLFormElement)
|
||||
)
|
||||
|
||||
if (newProjectName !== project.file.name) {
|
||||
if (newProjectName !== project.name) {
|
||||
send('Rename project', {
|
||||
data: { oldName: project.file.name, newName: newProjectName },
|
||||
data: { oldName: project.name, newName: newProjectName },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteProject(project: Project) {
|
||||
send('Delete project', { data: { name: project.file.name || '' } })
|
||||
send('Delete project', { data: { name: project.name || '' } })
|
||||
}
|
||||
|
||||
return (
|
||||
@ -296,7 +295,7 @@ const Home = () => {
|
||||
<ul className="grid w-full grid-cols-4 gap-4">
|
||||
{searchResults.sort(getSortFunction(sort)).map((project) => (
|
||||
<ProjectCard
|
||||
key={project.file.name}
|
||||
key={project.name}
|
||||
project={project}
|
||||
handleRenameProject={handleRenameProject}
|
||||
handleDeleteProject={handleDeleteProject}
|
||||
|
@ -913,7 +913,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(state.project.file.name, name);
|
||||
assert_eq!(state.project.file.path, tmp_project_dir.display().to_string());
|
||||
assert_eq!(state.project.path, tmp_project_dir.display().to_string());
|
||||
assert_eq!(
|
||||
state.current_file,
|
||||
Some(tmp_project_dir.join("main.kcl").display().to_string())
|
||||
@ -938,7 +938,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(state.project.file.name, name);
|
||||
assert_eq!(state.project.file.path, tmp_project_dir.display().to_string());
|
||||
assert_eq!(state.project.path, tmp_project_dir.display().to_string());
|
||||
assert_eq!(
|
||||
state.current_file,
|
||||
Some(tmp_project_dir.join("main.kcl").display().to_string())
|
||||
@ -963,7 +963,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(state.project.file.name, name);
|
||||
assert_eq!(state.project.file.path, tmp_project_dir.display().to_string());
|
||||
assert_eq!(state.project.path, tmp_project_dir.display().to_string());
|
||||
assert_eq!(
|
||||
state.current_file,
|
||||
Some(tmp_project_dir.join("main.kcl").display().to_string())
|
||||
@ -988,7 +988,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(state.project.file.name, name);
|
||||
assert_eq!(state.project.file.path, tmp_project_dir.display().to_string());
|
||||
assert_eq!(state.project.path, tmp_project_dir.display().to_string());
|
||||
assert_eq!(
|
||||
state.current_file,
|
||||
Some(tmp_project_dir.join("thing.kcl").display().to_string())
|
||||
@ -1013,7 +1013,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(state.project.file.name, name);
|
||||
assert_eq!(state.project.file.path, tmp_project_dir.display().to_string());
|
||||
assert_eq!(state.project.path, tmp_project_dir.display().to_string());
|
||||
assert_eq!(
|
||||
state.current_file,
|
||||
Some(tmp_project_dir.join("model.obj.kcl").display().to_string())
|
||||
|
@ -967,9 +967,9 @@ color = 1567.4"#;
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(project.file.name, project_name);
|
||||
assert_eq!(project.name, project_name);
|
||||
assert_eq!(
|
||||
project.file.path,
|
||||
project.path,
|
||||
settings
|
||||
.settings
|
||||
.project
|
||||
@ -981,7 +981,7 @@ color = 1567.4"#;
|
||||
assert_eq!(project.directory_count, 0);
|
||||
assert_eq!(
|
||||
project.default_file,
|
||||
std::path::Path::new(&project.file.path)
|
||||
std::path::Path::new(&project.path)
|
||||
.join(super::DEFAULT_PROJECT_KCL_FILE)
|
||||
.to_string_lossy()
|
||||
);
|
||||
@ -1017,9 +1017,9 @@ color = 1567.4"#;
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(project.file.name, project_name);
|
||||
assert_eq!(project.name, project_name);
|
||||
assert_eq!(
|
||||
project.file.path,
|
||||
project.path,
|
||||
settings
|
||||
.settings
|
||||
.project
|
||||
@ -1031,7 +1031,7 @@ color = 1567.4"#;
|
||||
assert_eq!(project.directory_count, 0);
|
||||
assert_eq!(
|
||||
project.default_file,
|
||||
std::path::Path::new(&project.file.path)
|
||||
std::path::Path::new(&project.path)
|
||||
.join(super::DEFAULT_PROJECT_KCL_FILE)
|
||||
.to_string_lossy()
|
||||
);
|
||||
@ -1057,8 +1057,8 @@ color = 1567.4"#;
|
||||
|
||||
let projects = settings.list_projects().await.unwrap();
|
||||
assert_eq!(projects.len(), 1);
|
||||
assert_eq!(projects[0].file.name, project_name);
|
||||
assert_eq!(projects[0].file.path, project.file.path);
|
||||
assert_eq!(projects[0].name, project_name);
|
||||
assert_eq!(projects[0].path, project.path);
|
||||
assert_eq!(projects[0].kcl_file_count, 1);
|
||||
assert_eq!(projects[0].directory_count, 0);
|
||||
assert_eq!(projects[0].default_file, project.default_file);
|
||||
@ -1084,8 +1084,8 @@ color = 1567.4"#;
|
||||
|
||||
let projects = settings.list_projects().await.unwrap();
|
||||
assert_eq!(projects.len(), 1);
|
||||
assert_eq!(projects[0].file.name, project_name);
|
||||
assert_eq!(projects[0].file.path, project.file.path);
|
||||
assert_eq!(projects[0].name, project_name);
|
||||
assert_eq!(projects[0].path, project.path);
|
||||
assert_eq!(projects[0].kcl_file_count, 1);
|
||||
assert_eq!(projects[0].directory_count, 0);
|
||||
assert_eq!(projects[0].default_file, project.default_file);
|
||||
@ -1111,8 +1111,8 @@ color = 1567.4"#;
|
||||
|
||||
let projects = settings.list_projects().await.unwrap();
|
||||
assert_eq!(projects.len(), 1);
|
||||
assert_eq!(projects[0].file.name, project_name);
|
||||
assert_eq!(projects[0].file.path, project.file.path);
|
||||
assert_eq!(projects[0].name, project_name);
|
||||
assert_eq!(projects[0].path, project.path);
|
||||
assert_eq!(projects[0].kcl_file_count, 1);
|
||||
assert_eq!(projects[0].directory_count, 0);
|
||||
assert_eq!(projects[0].default_file, project.default_file);
|
||||
@ -1138,8 +1138,8 @@ color = 1567.4"#;
|
||||
|
||||
let projects = settings.list_projects().await.unwrap();
|
||||
assert_eq!(projects.len(), 1);
|
||||
assert_eq!(projects[0].file.name, project_name);
|
||||
assert_eq!(projects[0].file.path, project.file.path);
|
||||
assert_eq!(projects[0].name, project_name);
|
||||
assert_eq!(projects[0].path, project.path);
|
||||
assert_eq!(projects[0].kcl_file_count, 1);
|
||||
assert_eq!(projects[0].directory_count, 0);
|
||||
assert_eq!(projects[0].default_file, project.default_file);
|
||||
|
69
vite.config.ts
Normal file
69
vite.config.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import react from '@vitejs/plugin-react'
|
||||
import viteTsconfigPaths from 'vite-tsconfig-paths'
|
||||
import eslint from 'vite-plugin-eslint'
|
||||
import { defineConfig, configDefaults } from 'vitest/config'
|
||||
import version from 'vite-plugin-package-version'
|
||||
// @ts-ignore: No types available
|
||||
import { lezer } from '@lezer/generator/rollup'
|
||||
|
||||
const config = defineConfig({
|
||||
server: {
|
||||
open: true,
|
||||
port: 3000,
|
||||
watch: {
|
||||
ignored: [
|
||||
'**/target/**',
|
||||
'**/dist/**',
|
||||
'**/build/**',
|
||||
'**/test-results/**',
|
||||
'**/playwright-report/**',
|
||||
],
|
||||
},
|
||||
},
|
||||
test: {
|
||||
globals: true,
|
||||
pool: 'forks',
|
||||
poolOptions: {
|
||||
forks: {
|
||||
maxForks: 2,
|
||||
minForks: 1,
|
||||
},
|
||||
},
|
||||
setupFiles: ['src/setupTests.ts', '@vitest/web-worker'],
|
||||
environment: 'happy-dom',
|
||||
coverage: {
|
||||
provider: 'istanbul', // or 'v8'
|
||||
},
|
||||
exclude: [...configDefaults.exclude, '**/e2e/**/*'],
|
||||
deps: {
|
||||
optimizer: {
|
||||
web: {
|
||||
include: ['vitest-canvas-mock'],
|
||||
},
|
||||
},
|
||||
},
|
||||
clearMocks: true,
|
||||
restoreMocks: true,
|
||||
mockReset: true,
|
||||
reporters: process.env.GITHUB_ACTIONS
|
||||
? ['dot', 'github-actions']
|
||||
: ['verbose', 'hanging-process'],
|
||||
testTimeout: 1000,
|
||||
hookTimeout: 1000,
|
||||
teardownTimeout: 1000,
|
||||
},
|
||||
build: {
|
||||
outDir: 'build',
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@kittycad/codemirror-lsp-client': '/packages/codemirror-lsp-client/src',
|
||||
},
|
||||
},
|
||||
plugins: [react(), viteTsconfigPaths(), eslint(), version(), lezer()],
|
||||
worker: {
|
||||
plugins: () => [viteTsconfigPaths()],
|
||||
},
|
||||
})
|
||||
|
||||
export default config
|
Reference in New Issue
Block a user