fmt
This commit is contained in:
@ -49,7 +49,9 @@ export const FileMachineProvider = ({
|
|||||||
commandBarSend({ type: 'Close' })
|
commandBarSend({ type: 'Close' })
|
||||||
navigate(
|
navigate(
|
||||||
`${paths.FILE}/${encodeURIComponent(
|
`${paths.FILE}/${encodeURIComponent(
|
||||||
context.selectedDirectory + window.electron.path.sep + event.data.name
|
context.selectedDirectory +
|
||||||
|
window.electron.path.sep +
|
||||||
|
event.data.name
|
||||||
)}`
|
)}`
|
||||||
)
|
)
|
||||||
} else if (
|
} else if (
|
||||||
@ -97,7 +99,10 @@ export const FileMachineProvider = ({
|
|||||||
let createdPath: string
|
let createdPath: string
|
||||||
|
|
||||||
if (event.data.makeDir) {
|
if (event.data.makeDir) {
|
||||||
createdPath = window.electron.path.join(context.selectedDirectory.path, createdName)
|
createdPath = window.electron.path.join(
|
||||||
|
context.selectedDirectory.path,
|
||||||
|
createdName
|
||||||
|
)
|
||||||
await window.electron.mkdir(createdPath)
|
await window.electron.mkdir(createdPath)
|
||||||
} else {
|
} else {
|
||||||
createdPath =
|
createdPath =
|
||||||
@ -119,8 +124,14 @@ export const FileMachineProvider = ({
|
|||||||
) => {
|
) => {
|
||||||
const { oldName, newName, isDir } = event.data
|
const { oldName, newName, isDir } = event.data
|
||||||
const name = newName ? newName : DEFAULT_FILE_NAME
|
const name = newName ? newName : DEFAULT_FILE_NAME
|
||||||
const oldPath = window.electron.path.join(context.selectedDirectory.path, oldName)
|
const oldPath = window.electron.path.join(
|
||||||
const newDirPath = window.electron.path.join(context.selectedDirectory.path, name)
|
context.selectedDirectory.path,
|
||||||
|
oldName
|
||||||
|
)
|
||||||
|
const newDirPath = window.electron.path.join(
|
||||||
|
context.selectedDirectory.path,
|
||||||
|
name
|
||||||
|
)
|
||||||
const newPath =
|
const newPath =
|
||||||
newDirPath + (name.endsWith(FILE_EXT) || isDir ? '' : FILE_EXT)
|
newDirPath + (name.endsWith(FILE_EXT) || isDir ? '' : FILE_EXT)
|
||||||
|
|
||||||
@ -152,13 +163,15 @@ export const FileMachineProvider = ({
|
|||||||
const isDir = !!event.data.children
|
const isDir = !!event.data.children
|
||||||
|
|
||||||
if (isDir) {
|
if (isDir) {
|
||||||
await window.electron.rm(event.data.path, {
|
await window.electron
|
||||||
recursive: true,
|
.rm(event.data.path, {
|
||||||
}).catch((e) => console.error('Error deleting directory', e))
|
recursive: true,
|
||||||
|
})
|
||||||
|
.catch((e) => console.error('Error deleting directory', e))
|
||||||
} else {
|
} else {
|
||||||
await window.electron.rm(event.data.path).catch((e) =>
|
await window.electron
|
||||||
console.error('Error deleting file', e)
|
.rm(event.data.path)
|
||||||
)
|
.catch((e) => console.error('Error deleting file', e))
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we just deleted the current file or one of its parent directories,
|
// If we just deleted the current file or one of its parent directories,
|
||||||
|
@ -207,7 +207,9 @@ function ProjectMenuPopover({
|
|||||||
<div className="flex flex-col items-start py-0.5">
|
<div className="flex flex-col items-start py-0.5">
|
||||||
<span className="hidden text-sm text-chalkboard-110 dark:text-chalkboard-20 whitespace-nowrap lg:block">
|
<span className="hidden text-sm text-chalkboard-110 dark:text-chalkboard-20 whitespace-nowrap lg:block">
|
||||||
{isDesktop() && file?.name
|
{isDesktop() && file?.name
|
||||||
? file.name.slice(file.name.lastIndexOf(window.electron.path.sep) + 1)
|
? file.name.slice(
|
||||||
|
file.name.lastIndexOf(window.electron.path.sep) + 1
|
||||||
|
)
|
||||||
: APP_NAME}
|
: APP_NAME}
|
||||||
</span>
|
</span>
|
||||||
{isDesktop() && project?.name && (
|
{isDesktop() && project?.name && (
|
||||||
|
@ -46,7 +46,12 @@ export const AllSettingsFields = forwardRef(
|
|||||||
location.pathname
|
location.pathname
|
||||||
.replace(paths.FILE + '/', '')
|
.replace(paths.FILE + '/', '')
|
||||||
.replace(paths.SETTINGS, '')
|
.replace(paths.SETTINGS, '')
|
||||||
.slice(0, decodeURI(location.pathname).lastIndexOf(window.electron.path.sep))
|
.slice(
|
||||||
|
0,
|
||||||
|
decodeURI(location.pathname).lastIndexOf(
|
||||||
|
window.electron.path.sep
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
|
@ -119,11 +119,13 @@ export default class CodeManager {
|
|||||||
// Wait one event loop to give a chance for params to be set
|
// Wait one event loop to give a chance for params to be set
|
||||||
// Save the file to disk
|
// Save the file to disk
|
||||||
this._currentFilePath &&
|
this._currentFilePath &&
|
||||||
window.electron.writeFile(this._currentFilePath, this.code ?? '').catch((err) => {
|
window.electron
|
||||||
// TODO: add tracing per GH issue #254 (https://github.com/KittyCAD/modeling-app/issues/254)
|
.writeFile(this._currentFilePath, this.code ?? '')
|
||||||
console.error('error saving file', err)
|
.catch((err) => {
|
||||||
toast.error('Error saving file, please check file permissions')
|
// TODO: add tracing 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')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
safeLSSetItem(PERSIST_CODE_KEY, this.code)
|
safeLSSetItem(PERSIST_CODE_KEY, this.code)
|
||||||
|
@ -52,8 +52,9 @@ class FileSystemManager {
|
|||||||
return Promise.reject(new Error(`Error checking file exists: ${error}`))
|
return Promise.reject(new Error(`Error checking file exists: ${error}`))
|
||||||
})
|
})
|
||||||
.then(async (file) => {
|
.then(async (file) => {
|
||||||
try { await window.electron.stat(file) }
|
try {
|
||||||
catch (e) {
|
await window.electron.stat(file)
|
||||||
|
} catch (e) {
|
||||||
if (e === 'ENOENT') {
|
if (e === 'ENOENT') {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -77,7 +78,8 @@ class FileSystemManager {
|
|||||||
return Promise.reject(new Error(`Error joining dir: ${error}`))
|
return Promise.reject(new Error(`Error joining dir: ${error}`))
|
||||||
})
|
})
|
||||||
.then((filepath) => {
|
.then((filepath) => {
|
||||||
return window.electron.readdir(filepath)
|
return window.electron
|
||||||
|
.readdir(filepath)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
return Promise.reject(new Error(`Error reading dir: ${error}`))
|
return Promise.reject(new Error(`Error reading dir: ${error}`))
|
||||||
})
|
})
|
||||||
|
@ -13,15 +13,13 @@ import {
|
|||||||
parseAppSettings,
|
parseAppSettings,
|
||||||
parseProjectSettings,
|
parseProjectSettings,
|
||||||
} from 'lang/wasm'
|
} from 'lang/wasm'
|
||||||
export {
|
export { parseProjectRoute } from 'lang/wasm'
|
||||||
parseProjectRoute,
|
|
||||||
} from 'lang/wasm'
|
|
||||||
|
|
||||||
const DEFAULT_HOST = 'https://api.zoo.dev'
|
const DEFAULT_HOST = 'https://api.zoo.dev'
|
||||||
const SETTINGS_FILE_NAME = 'settings.toml'
|
const SETTINGS_FILE_NAME = 'settings.toml'
|
||||||
const PROJECT_SETTINGS_FILE_NAME = 'project.toml'
|
const PROJECT_SETTINGS_FILE_NAME = 'project.toml'
|
||||||
const PROJECT_FOLDER = 'zoo-modeling-app-projects'
|
const PROJECT_FOLDER = 'zoo-modeling-app-projects'
|
||||||
const DEFAULT_PROJECT_KCL_FILE = "main.kcl"
|
const DEFAULT_PROJECT_KCL_FILE = 'main.kcl'
|
||||||
|
|
||||||
// List machines on the local network.
|
// List machines on the local network.
|
||||||
export async function listMachines(): Promise<{
|
export async function listMachines(): Promise<{
|
||||||
@ -44,20 +42,28 @@ export async function renameProjectDirectory(
|
|||||||
return Promise.reject(new Error(`New name for project cannot be empty`))
|
return Promise.reject(new Error(`New name for project cannot be empty`))
|
||||||
}
|
}
|
||||||
|
|
||||||
try { await window.electron.stat(projectPath) }
|
try {
|
||||||
catch (e) {
|
await window.electron.stat(projectPath)
|
||||||
|
} catch (e) {
|
||||||
if (e === 'ENOENT') {
|
if (e === 'ENOENT') {
|
||||||
return Promise.reject(new Error(`Path ${projectPath} is not a directory`))
|
return Promise.reject(new Error(`Path ${projectPath} is not a directory`))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure the new name does not exist.
|
// Make sure the new name does not exist.
|
||||||
const newPath = window.electron.path.join(projectPath.split('/').slice(0, -1).join('/'), newName)
|
const newPath = window.electron.path.join(
|
||||||
|
projectPath.split('/').slice(0, -1).join('/'),
|
||||||
|
newName
|
||||||
|
)
|
||||||
try {
|
try {
|
||||||
await window.electron.stat(newPath)
|
await window.electron.stat(newPath)
|
||||||
// If we get here it means the stat succeeded and there's a file already
|
// If we get here it means the stat succeeded and there's a file already
|
||||||
// with the same name...
|
// with the same name...
|
||||||
return Promise.reject(new Error(`Path ${newPath} already exists, cannot rename to an existing path`))
|
return Promise.reject(
|
||||||
|
new Error(
|
||||||
|
`Path ${newPath} already exists, cannot rename to an existing path`
|
||||||
|
)
|
||||||
|
)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Otherwise if it failed and the failure is "it doesnt exist" then rename it!
|
// Otherwise if it failed and the failure is "it doesnt exist" then rename it!
|
||||||
if (e === 'ENOENT') {
|
if (e === 'ENOENT') {
|
||||||
@ -143,11 +149,15 @@ export async function listProjects(
|
|||||||
const projectPath = window.electron.path.join(projectDir, entry)
|
const projectPath = window.electron.path.join(projectDir, entry)
|
||||||
// if it's not a directory ignore.
|
// if it's not a directory ignore.
|
||||||
const isDirectory = await window.electron.statIsDirectory(projectPath)
|
const isDirectory = await window.electron.statIsDirectory(projectPath)
|
||||||
if (!isDirectory) { continue }
|
if (!isDirectory) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
const project = await getProjectInfo(projectPath)
|
const project = await getProjectInfo(projectPath)
|
||||||
// Needs at least one file to be added to the projects list
|
// Needs at least one file to be added to the projects list
|
||||||
if (project.kcl_file_count === 0) { continue }
|
if (project.kcl_file_count === 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
projects.push(project)
|
projects.push(project)
|
||||||
}
|
}
|
||||||
return projects
|
return projects
|
||||||
@ -155,90 +165,104 @@ export async function listProjects(
|
|||||||
|
|
||||||
const IMPORT_FILE_EXTENSIONS = [
|
const IMPORT_FILE_EXTENSIONS = [
|
||||||
// TODO Use ImportFormat enum
|
// TODO Use ImportFormat enum
|
||||||
"stp", "glb", "fbxb", "kcl"
|
'stp',
|
||||||
|
'glb',
|
||||||
|
'fbxb',
|
||||||
|
'kcl',
|
||||||
]
|
]
|
||||||
|
|
||||||
const isRelevantFile = (filename: string): boolean => IMPORT_FILE_EXTENSIONS.some(
|
const isRelevantFile = (filename: string): boolean =>
|
||||||
(ext) => filename.endsWith('.' + ext))
|
IMPORT_FILE_EXTENSIONS.some((ext) => filename.endsWith('.' + ext))
|
||||||
|
|
||||||
const collectAllFilesRecursiveFrom = async (path: string) => {
|
const collectAllFilesRecursiveFrom = async (path: string) => {
|
||||||
// Make sure the filesystem object exists.
|
// Make sure the filesystem object exists.
|
||||||
try { await window.electron.stat(path) }
|
try {
|
||||||
catch (e) {
|
await window.electron.stat(path)
|
||||||
if (e === 'ENOENT') {
|
} catch (e) {
|
||||||
return Promise.reject(new Error(`Directory ${path} does not exist`))
|
if (e === 'ENOENT') {
|
||||||
|
return Promise.reject(new Error(`Directory ${path} does not exist`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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`))
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathParts = path.split('/')
|
||||||
|
let entry = /* FileEntry */ {
|
||||||
|
name: pathParts.slice(-1)[0],
|
||||||
|
path,
|
||||||
|
children: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
const children = []
|
||||||
|
|
||||||
|
const entries = await window.electron.readdir(path)
|
||||||
|
|
||||||
|
for (let e of entries) {
|
||||||
|
// ignore hidden files and directories (starting with a dot)
|
||||||
|
if (e.indexOf('.') === 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const ePath = window.electron.path.join(path, e)
|
||||||
|
const isEDir = await window.electron.statIsDirectory(ePath)
|
||||||
|
|
||||||
|
if (isEDir) {
|
||||||
|
const subChildren = await collectAllFilesRecursiveFrom(ePath)
|
||||||
|
children.push(subChildren)
|
||||||
|
} else {
|
||||||
|
if (!isRelevantFile(ePath)) {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
}
|
children.push(
|
||||||
|
/* FileEntry */ {
|
||||||
// Make sure the path is a directory.
|
name: e,
|
||||||
const isPathDir = await window.electron.statIsDirectory(path)
|
path: ePath,
|
||||||
if (!isPathDir) {
|
children: undefined,
|
||||||
return Promise.reject(new Error(`Path ${path} is not a directory`))
|
|
||||||
}
|
|
||||||
|
|
||||||
const pathParts = path.split('/')
|
|
||||||
let entry = /* FileEntry */ {
|
|
||||||
name: pathParts.slice(-1)[0],
|
|
||||||
path,
|
|
||||||
children: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
const children = []
|
|
||||||
|
|
||||||
const entries = await window.electron.readdir(path)
|
|
||||||
|
|
||||||
for (let e of entries) {
|
|
||||||
// ignore hidden files and directories (starting with a dot)
|
|
||||||
if (e.indexOf('.') === 0) { continue }
|
|
||||||
|
|
||||||
const ePath = window.electron.path.join(path, e)
|
|
||||||
const isEDir = await window.electron.statIsDirectory(ePath)
|
|
||||||
|
|
||||||
if (isEDir) {
|
|
||||||
const subChildren = await collectAllFilesRecursiveFrom(ePath)
|
|
||||||
children.push(subChildren)
|
|
||||||
} else {
|
|
||||||
if (!isRelevantFile(ePath)) { continue }
|
|
||||||
children.push(/* FileEntry */ {
|
|
||||||
name: e,
|
|
||||||
path: ePath,
|
|
||||||
children: undefined,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// We don't set this to none if there are no children, because it's a directory.
|
// We don't set this to none if there are no children, because it's a directory.
|
||||||
entry.children = children
|
entry.children = children
|
||||||
|
|
||||||
return entry
|
return entry
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDefaultKclFileForDir = async (projectDir, file) => {
|
const getDefaultKclFileForDir = async (projectDir, file) => {
|
||||||
// Make sure the dir is a directory.
|
// Make sure the dir is a directory.
|
||||||
const isFileEntryDir = await window.electron.statIsDirectory(projectDir)
|
const isFileEntryDir = await window.electron.statIsDirectory(projectDir)
|
||||||
if (!isFileEntryDir) {
|
if (!isFileEntryDir) {
|
||||||
return Promise.reject(new Error(`Path ${projectDir} is not a directory`))
|
return Promise.reject(new Error(`Path ${projectDir} is not a directory`))
|
||||||
}
|
}
|
||||||
|
|
||||||
let defaultFilePath = window.electron.path.join(projectDir, DEFAULT_PROJECT_KCL_FILE)
|
let defaultFilePath = window.electron.path.join(
|
||||||
try { await window.electron.stat(defaultFilePath) }
|
projectDir,
|
||||||
catch (e) {
|
DEFAULT_PROJECT_KCL_FILE
|
||||||
if (e === 'ENOENT') {
|
)
|
||||||
// Find a kcl file in the directory.
|
try {
|
||||||
if (file.children) {
|
await window.electron.stat(defaultFilePath)
|
||||||
for (let entry of file.children) {
|
} catch (e) {
|
||||||
if (entry.name.endsWith(".kcl")) {
|
if (e === 'ENOENT') {
|
||||||
return window.electron.path.join(projectDir, entry.name)
|
// Find a kcl file in the directory.
|
||||||
} else if (entry.children.length > 0) {
|
if (file.children) {
|
||||||
// Recursively find a kcl file in the directory.
|
for (let entry of file.children) {
|
||||||
return getDefaultKclFileForDir(entry.path, entry)
|
if (entry.name.endsWith('.kcl')) {
|
||||||
}
|
return window.electron.path.join(projectDir, entry.name)
|
||||||
}
|
} else if (entry.children.length > 0) {
|
||||||
// If we didn't find a kcl file, create one.
|
// Recursively find a kcl file in the directory.
|
||||||
await window.electron.writeFile(defaultFilePath, '')
|
return getDefaultKclFileForDir(entry.path, entry)
|
||||||
return defaultFilePath
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// If we didn't find a kcl file, create one.
|
||||||
|
await window.electron.writeFile(defaultFilePath, '')
|
||||||
|
return defaultFilePath
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!file.children) {
|
if (!file.children) {
|
||||||
@ -249,70 +273,72 @@ const getDefaultKclFileForDir = async (projectDir, file) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const kclFileCount = (file /* fileEntry */) => {
|
const kclFileCount = (file /* fileEntry */) => {
|
||||||
let count = 0
|
let count = 0
|
||||||
if (file.children) {
|
if (file.children) {
|
||||||
for (let entry of file.children) {
|
for (let entry of file.children) {
|
||||||
if (entry.name.endsWith(".kcl")) {
|
if (entry.name.endsWith('.kcl')) {
|
||||||
count += 1
|
count += 1
|
||||||
} else {
|
} else {
|
||||||
count += kclFileCount(entry)
|
count += kclFileCount(entry)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Populate the number of directories in the project.
|
/// Populate the number of directories in the project.
|
||||||
const directoryCount = (file /* FileEntry */) => {
|
const directoryCount = (file /* FileEntry */) => {
|
||||||
let count = 0
|
let count = 0
|
||||||
if (file.children) {
|
if (file.children) {
|
||||||
for (let entry of file.children) {
|
for (let entry of file.children) {
|
||||||
count += 1
|
count += 1
|
||||||
directoryCount(entry)
|
directoryCount(entry)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getProjectInfo(projectPath: string): Promise<Project> {
|
||||||
export async function getProjectInfo(
|
// Check the directory.
|
||||||
projectPath: string,
|
try {
|
||||||
): Promise<Project> {
|
await window.electron.stat(projectPath)
|
||||||
// Check the directory.
|
} catch (e) {
|
||||||
try { await window.electron.stat(projectPath) }
|
if (e === 'ENOENT') {
|
||||||
catch (e) {
|
return Promise.reject(
|
||||||
if (e === 'ENOENT') {
|
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.
|
// Make sure it is a directory.
|
||||||
const projectPathIsDir = await window.electron.statIsDirectory(projectPath)
|
const projectPathIsDir = await window.electron.statIsDirectory(projectPath)
|
||||||
if (!projectPathIsDir) {
|
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 walked = await collectAllFilesRecursiveFrom(projectPath)
|
||||||
let default_file = await getDefaultKclFileForDir(projectPath, walked)
|
let default_file = await getDefaultKclFileForDir(projectPath, walked)
|
||||||
const metadata = await window.electron.stat(projectPath)
|
const metadata = await window.electron.stat(projectPath)
|
||||||
|
|
||||||
let project = /* FileEntry */ {
|
let project = /* FileEntry */ {
|
||||||
...walked,
|
...walked,
|
||||||
metadata,
|
metadata,
|
||||||
kcl_file_count: 0,
|
kcl_file_count: 0,
|
||||||
directory_count: 0,
|
directory_count: 0,
|
||||||
default_file,
|
default_file,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate the number of KCL files in the project.
|
// Populate the number of KCL files in the project.
|
||||||
project.kcl_file_count = kclFileCount(project)
|
project.kcl_file_count = kclFileCount(project)
|
||||||
|
|
||||||
//Populate the number of directories in the project.
|
//Populate the number of directories in the project.
|
||||||
project.directory_count = directoryCount(project)
|
project.directory_count = directoryCount(project)
|
||||||
|
|
||||||
return project
|
return project
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write project settings file.
|
// Write project settings file.
|
||||||
@ -361,18 +387,20 @@ export const getInitialDefaultDir = async () => {
|
|||||||
return window.electron.path.join(dir, PROJECT_FOLDER)
|
return window.electron.path.join(dir, PROJECT_FOLDER)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const readProjectSettingsFile = async (projectPath: string): ProjectConfiguration => {
|
export const readProjectSettingsFile = async (
|
||||||
let settingsPath = await getProjectSettingsFilePath(projectPath)
|
projectPath: string
|
||||||
|
): ProjectConfiguration => {
|
||||||
|
let settingsPath = await getProjectSettingsFilePath(projectPath)
|
||||||
|
|
||||||
// Check if this file exists.
|
// Check if this file exists.
|
||||||
try { await window.electron.stat(settingsPath) }
|
try {
|
||||||
catch (e) {
|
await window.electron.stat(settingsPath)
|
||||||
if (e === 'ENOENT') {
|
} catch (e) {
|
||||||
// Return the default configuration.
|
if (e === 'ENOENT') {
|
||||||
return {}
|
// Return the default configuration.
|
||||||
}
|
return {}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const configToml = await window.electron.readFile(settingsPath)
|
const configToml = await window.electron.readFile(settingsPath)
|
||||||
const configObj = parseProjectSettings(configToml)
|
const configObj = parseProjectSettings(configToml)
|
||||||
@ -444,4 +472,3 @@ export const getUser = async (
|
|||||||
})
|
})
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,7 +63,10 @@ function interpolateProjectName(projectName: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Returns the next available index for a project name
|
// Returns the next available index for a project name
|
||||||
export function getNextProjectIndex(projectName: string, projects: FileEntry[]) {
|
export function getNextProjectIndex(
|
||||||
|
projectName: string,
|
||||||
|
projects: FileEntry[]
|
||||||
|
) {
|
||||||
const regex = interpolateProjectName(projectName)
|
const regex = interpolateProjectName(projectName)
|
||||||
const matches = projects.map((project) => project.name?.match(regex))
|
const matches = projects.map((project) => project.name?.match(regex))
|
||||||
const indices = matches
|
const indices = matches
|
||||||
|
@ -6,7 +6,8 @@ import packageJson from '../../package.json'
|
|||||||
const open = (args: any) => ipcRenderer.invoke('dialog.showOpenDialog', args)
|
const open = (args: any) => ipcRenderer.invoke('dialog.showOpenDialog', args)
|
||||||
const save = (args: any) => ipcRenderer.invoke('dialog.showSaveDialog', args)
|
const save = (args: any) => ipcRenderer.invoke('dialog.showSaveDialog', args)
|
||||||
const openExternal = (url: any) => ipcRenderer.invoke('shell.openExternal', url)
|
const openExternal = (url: any) => ipcRenderer.invoke('shell.openExternal', url)
|
||||||
const showInFolder = (path: string) => ipcRenderer.invoke('shell.showItemInFolder', path)
|
const showInFolder = (path: string) =>
|
||||||
|
ipcRenderer.invoke('shell.showItemInFolder', path)
|
||||||
const login = (host: string) => ipcRenderer.invoke('login', host)
|
const login = (host: string) => ipcRenderer.invoke('login', host)
|
||||||
|
|
||||||
const readFile = (path: string) => fs.readFile(path, 'utf-8')
|
const readFile = (path: string) => fs.readFile(path, 'utf-8')
|
||||||
@ -14,10 +15,12 @@ const rename = (prev: string, next: string) => fs.rename(prev, next)
|
|||||||
const writeFile = (path: string, data: string) =>
|
const writeFile = (path: string, data: string) =>
|
||||||
fs.writeFile(path, data, 'utf-8')
|
fs.writeFile(path, data, 'utf-8')
|
||||||
const readdir = (path: string) => fs.readdir(path, 'utf-8')
|
const readdir = (path: string) => fs.readdir(path, 'utf-8')
|
||||||
const stat = (path: string) => fs.stat(path).catch((e) => Promise.reject(e.code))
|
const stat = (path: string) =>
|
||||||
|
fs.stat(path).catch((e) => Promise.reject(e.code))
|
||||||
// Electron has behavior where it doesn't clone the prototype chain over.
|
// Electron has behavior where it doesn't clone the prototype chain over.
|
||||||
// So we need to call stat.isDirectory on this side.
|
// So we need to call stat.isDirectory on this side.
|
||||||
const statIsDirectory = (path: string) => stat(path).then((res) => res.isDirectory())
|
const statIsDirectory = (path: string) =>
|
||||||
|
stat(path).then((res) => res.isDirectory())
|
||||||
const getPath = async (name: string) => ipcRenderer.invoke('app.getPath', name)
|
const getPath = async (name: string) => ipcRenderer.invoke('app.getPath', name)
|
||||||
|
|
||||||
const exposeProcessEnv = (varName: string) => {
|
const exposeProcessEnv = (varName: string) => {
|
||||||
|
@ -30,7 +30,10 @@ const save_ = async (file: ModelingAppFile) => {
|
|||||||
if (filePathMeta.canceled) return
|
if (filePathMeta.canceled) return
|
||||||
|
|
||||||
// Write the file.
|
// Write the file.
|
||||||
await window.electron.writeFile(filePathMeta.filePath, new Uint8Array(file.contents))
|
await window.electron.writeFile(
|
||||||
|
filePathMeta.filePath,
|
||||||
|
new Uint8Array(file.contents)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
// Download the file to the user's computer.
|
// Download the file to the user's computer.
|
||||||
// Now we need to download the files to the user's downloads folder.
|
// Now we need to download the files to the user's downloads folder.
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import { isDesktop } from 'lib/isDesktop'
|
import { isDesktop } from 'lib/isDesktop'
|
||||||
|
|
||||||
export const openExternalBrowserIfDesktop = (to) => function(e) {
|
export const openExternalBrowserIfDesktop = (to) =>
|
||||||
if (isDesktop()) {
|
function (e) {
|
||||||
window.electron.openExternal(to || e.currentTarget.href)
|
if (isDesktop()) {
|
||||||
e.preventDefault()
|
window.electron.openExternal(to || e.currentTarget.href)
|
||||||
e.stopPropagation()
|
e.preventDefault()
|
||||||
return false
|
e.stopPropagation()
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Open a new browser window desktop style or browser style.
|
// Open a new browser window desktop style or browser style.
|
||||||
export default async function openWindow(url: string) {
|
export default async function openWindow(url: string) {
|
||||||
|
@ -70,7 +70,9 @@ export const onboardingRedirectLoader: ActionFunction = async (args) => {
|
|||||||
return settingsLoader(args)
|
return settingsLoader(args)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fileLoader: LoaderFunction = async (routerData): Promise<FileLoaderData | Response> => {
|
export const fileLoader: LoaderFunction = async (
|
||||||
|
routerData
|
||||||
|
): Promise<FileLoaderData | Response> => {
|
||||||
const { params } = routerData
|
const { params } = routerData
|
||||||
let { configuration } = await loadAndValidateSettings()
|
let { configuration } = await loadAndValidateSettings()
|
||||||
|
|
||||||
@ -93,7 +95,9 @@ export const fileLoader: LoaderFunction = async (routerData): Promise<FileLoader
|
|||||||
if (!current_file_name || !current_file_path || !project_name) {
|
if (!current_file_name || !current_file_path || !project_name) {
|
||||||
return redirect(
|
return redirect(
|
||||||
`${paths.FILE}/${encodeURIComponent(
|
`${paths.FILE}/${encodeURIComponent(
|
||||||
`${params.id}${isDesktop() ? window.electron.path.sep : '/'}${PROJECT_ENTRYPOINT}`
|
`${params.id}${
|
||||||
|
isDesktop() ? window.electron.path.sep : '/'
|
||||||
|
}${PROJECT_ENTRYPOINT}`
|
||||||
)}`
|
)}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -209,14 +209,15 @@ export function createSettings() {
|
|||||||
// In desktop end-to-end tests we can't control the file picker,
|
// In desktop end-to-end tests we can't control the file picker,
|
||||||
// so we seed the new directory value in the element's dataset
|
// so we seed the new directory value in the element's dataset
|
||||||
const inputRefVal = inputRef.current?.dataset.testValue
|
const inputRefVal = inputRef.current?.dataset.testValue
|
||||||
if (inputRef.current && inputRefVal && !Array.isArray(inputRefVal)) {
|
if (
|
||||||
|
inputRef.current &&
|
||||||
|
inputRefVal &&
|
||||||
|
!Array.isArray(inputRefVal)
|
||||||
|
) {
|
||||||
updateValue(inputRefVal)
|
updateValue(inputRefVal)
|
||||||
} else {
|
} else {
|
||||||
const newPath = await window.electron.open({
|
const newPath = await window.electron.open({
|
||||||
properties: [
|
properties: ['openDirectory', 'createDirectory'],
|
||||||
'openDirectory',
|
|
||||||
'createDirectory',
|
|
||||||
],
|
|
||||||
defaultPath: value,
|
defaultPath: value,
|
||||||
title: 'Choose a new project directory',
|
title: 'Choose a new project directory',
|
||||||
})
|
})
|
||||||
|
@ -237,7 +237,10 @@ export async function saveSettings(
|
|||||||
|
|
||||||
// Write the project settings.
|
// Write the project settings.
|
||||||
if (onDesktop) {
|
if (onDesktop) {
|
||||||
await writeProjectSettingsFile({ projectPath, configuration: { settings: projectSettings }})
|
await writeProjectSettingsFile({
|
||||||
|
projectPath,
|
||||||
|
configuration: { settings: projectSettings },
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
localStorage.setItem(localStorageProjectSettingsPath(), tomlStr)
|
localStorage.setItem(localStorageProjectSettingsPath(), tomlStr)
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import { getNextProjectIndex, interpolateProjectNameWithIndex } from './desktopFS'
|
import {
|
||||||
|
getNextProjectIndex,
|
||||||
|
interpolateProjectNameWithIndex,
|
||||||
|
} from './desktopFS'
|
||||||
import { MAX_PADDING } from './constants'
|
import { MAX_PADDING } from './constants'
|
||||||
|
|
||||||
describe('Test project name utility functions', () => {
|
describe('Test project name utility functions', () => {
|
||||||
|
@ -146,7 +146,7 @@ async function getUser(context: UserContext) {
|
|||||||
})
|
})
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.catch((err) => console.error('error from Browser getUser', err))
|
.catch((err) => console.error('error from Browser getUser', err))
|
||||||
: getUserDesktop(context.token, VITE_KC_API_BASE_URL )
|
: getUserDesktop(context.token, VITE_KC_API_BASE_URL)
|
||||||
|
|
||||||
const user = await userPromise
|
const user = await userPromise
|
||||||
|
|
||||||
|
74
src/main.ts
74
src/main.ts
@ -71,45 +71,47 @@ ipcMain.handle('shell.openExternal', (event, data) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('login', async (event, host) => {
|
ipcMain.handle('login', async (event, host) => {
|
||||||
console.log('Logging in...')
|
console.log('Logging in...')
|
||||||
// Do an OAuth 2.0 Device Authorization Grant dance to get a token.
|
// Do an OAuth 2.0 Device Authorization Grant dance to get a token.
|
||||||
const issuer = new Issuer({
|
const issuer = new Issuer({
|
||||||
device_authorization_endpoint: `${host}/oauth2/device/auth`,
|
device_authorization_endpoint: `${host}/oauth2/device/auth`,
|
||||||
token_endpoint: `${host}/oauth2/device/token`,
|
token_endpoint: `${host}/oauth2/device/token`,
|
||||||
})
|
})
|
||||||
const client = new issuer.Client({
|
const client = new issuer.Client({
|
||||||
// We can hardcode the client ID.
|
// We can hardcode the client ID.
|
||||||
// This value is safe to be embedded in version control.
|
// This value is safe to be embedded in version control.
|
||||||
// This is the client ID of the KittyCAD app.
|
// This is the client ID of the KittyCAD app.
|
||||||
client_id: "2af127fb-e14e-400a-9c57-a9ed08d1a5b7",
|
client_id: '2af127fb-e14e-400a-9c57-a9ed08d1a5b7',
|
||||||
token_endpoint_auth_method: 'none',
|
token_endpoint_auth_method: 'none',
|
||||||
})
|
})
|
||||||
|
|
||||||
const handle = await client.deviceAuthorization()
|
const handle = await client.deviceAuthorization()
|
||||||
|
|
||||||
// Open the system browser with the auth_uri.
|
// Open the system browser with the auth_uri.
|
||||||
// We do this in the browser and not a separate window because we want 1password and
|
// We do this in the browser and not a separate window because we want 1password and
|
||||||
// other crap to work well.
|
// other crap to work well.
|
||||||
// TODO: find a better way to share this value with tauri e2e tests
|
// TODO: find a better way to share this value with tauri e2e tests
|
||||||
// Here we're using an env var to enable the /tmp file (windows not supported for now)
|
// Here we're using an env var to enable the /tmp file (windows not supported for now)
|
||||||
// and bypass the shell::open call as it fails on GitHub Actions.
|
// and bypass the shell::open call as it fails on GitHub Actions.
|
||||||
const e2e_tauri_enabled = process.env.E2E_TAURI_ENABLED
|
const e2e_tauri_enabled = process.env.E2E_TAURI_ENABLED
|
||||||
if (e2e_tauri_enabled) {
|
if (e2e_tauri_enabled) {
|
||||||
console.warn(`E2E_TAURI_ENABLED is set, won't open ${handle.verification_uri_complete} externally`)
|
console.warn(
|
||||||
let temp = '/tmp'
|
`E2E_TAURI_ENABLED is set, won't open ${handle.verification_uri_complete} externally`
|
||||||
// Overwrite with Windows variable
|
)
|
||||||
if (process.env.TEMP) {
|
let temp = '/tmp'
|
||||||
temp = process.env.TEMP
|
// Overwrite with Windows variable
|
||||||
}
|
if (process.env.TEMP) {
|
||||||
let path = path.join(temp, "kittycad_user_code")
|
temp = process.env.TEMP
|
||||||
console.log(`Writing to ${path}`)
|
|
||||||
await fs.writeFile(path, handle.user_code)
|
|
||||||
} else {
|
|
||||||
shell.openExternal(handle.verification_uri_complete)
|
|
||||||
}
|
}
|
||||||
|
let path = path.join(temp, 'kittycad_user_code')
|
||||||
|
console.log(`Writing to ${path}`)
|
||||||
|
await fs.writeFile(path, handle.user_code)
|
||||||
|
} else {
|
||||||
|
shell.openExternal(handle.verification_uri_complete)
|
||||||
|
}
|
||||||
|
|
||||||
// Wait for the user to login.
|
// Wait for the user to login.
|
||||||
const tokenSet = await handle.poll()
|
const tokenSet = await handle.poll()
|
||||||
|
|
||||||
return tokenSet.access_token
|
return tokenSet.access_token
|
||||||
})
|
})
|
||||||
|
@ -80,7 +80,10 @@ const Home = () => {
|
|||||||
event: EventFrom<typeof homeMachine>
|
event: EventFrom<typeof homeMachine>
|
||||||
) => {
|
) => {
|
||||||
if (event.data && 'name' in event.data) {
|
if (event.data && 'name' in event.data) {
|
||||||
let projectPath = context.defaultDirectory + window.electron.path.sep + event.data.name
|
let projectPath =
|
||||||
|
context.defaultDirectory +
|
||||||
|
window.electron.path.sep +
|
||||||
|
event.data.name
|
||||||
onProjectOpen(
|
onProjectOpen(
|
||||||
{
|
{
|
||||||
name: event.data.name,
|
name: event.data.name,
|
||||||
@ -138,9 +141,12 @@ const Home = () => {
|
|||||||
context: ContextFrom<typeof homeMachine>,
|
context: ContextFrom<typeof homeMachine>,
|
||||||
event: EventFrom<typeof homeMachine, 'Delete project'>
|
event: EventFrom<typeof homeMachine, 'Delete project'>
|
||||||
) => {
|
) => {
|
||||||
await window.electron.rm(window.electron.path.join(context.defaultDirectory, event.data.name), {
|
await window.electron.rm(
|
||||||
recursive: true,
|
window.electron.path.join(context.defaultDirectory, event.data.name),
|
||||||
})
|
{
|
||||||
|
recursive: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
return `Successfully deleted "${event.data.name}"`
|
return `Successfully deleted "${event.data.name}"`
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user