{"version":3,"mappings":"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","names":["buildFileTree","fileMetadata","output","a","b","metadata","path","splitPath","endsWithSlash","label","index","fileName","isFile","folders","item","files","createFormDataBlob","content","type","blobManifest","formData","useVfsStore","defineStore","fileListArray","_path","userId","projectId","version","list","pyscriptApi","mappedArray","project","isFolder","allPaths","pathString","p","backup","filePath","oldPath","newPath","oldFileContent","value","_a","_b","resp","contentType","blob","resolve","reject","reader","e","downloadUrl","link","fullPath","parsedFilePath","newFilePath","file","uploadedFileMetadata","folderPath","filePathToDelete","restore","err","folderToDeleteAsString","keyPath","_valueMetadata","pathFrom","pathTo","skipOverwriteConfirmation","shouldOverwrite","hasFile","pathFromMetadata","pathFromParentPath","folderName","_key","pathToFolder","hasFolderConflict","confirmResult","filePathFrom","newPathTo","newBasename","oldMetadata","newMetadata","plainToInstance","FileMetadata","oldPathAsString","valueMetadata","oldPathWithoutTrailingSlash","newPathWithoutTrailingSlash","basename","MAX_FILE_SIZE","MAX_FILE_SIZE_TEXT","FILE_IGNORE_LIST","tempFileMetadata","dirname","show","props","__props","projectStore","useProjectStore","vfsStore","ProjectPageEditable","defineAsyncComponent","__vitePreload","ProjectPageReadOnly","ProjectPageInvent","NotFoundView","pageTitle","versionData","computed","v","useHead","state","reactive","useEventListener","onBeforeMount","resetStores","show404Page","onBeforeRouteUpdate","to","from"],"sources":["../../../ui/src/utilities/build-file-tree.ts","../../../ui/src/utilities/create-form-data-blob.ts","../../../ui/src/stores/vfs.ts","../../../ui/src/views/projects/ProjectView.vue"],"sourcesContent":["import type { FileMetadata } from '~/utilities/pyscript-api-models';\n\nexport type TreeNode = FileNodeType | FolderNodeType;\n\nexport interface FileNodeType {\n children: TreeNode[];\n label: string;\n type: 'file';\n metadata: FileMetadata;\n}\n\nexport interface FolderNodeType {\n children: TreeNode[];\n label: string;\n type: 'folder';\n dirname: string;\n}\n\nexport interface Output {\n tree: TreeNode[];\n cache: Record<string, Output>;\n}\n\n/**\n * Builds a file tree object by utilizing the path of each file.\n */\nexport function buildFileTree(fileMetadata: FileMetadata[]): TreeNode[] {\n const output: Output = { tree: [], cache: {} };\n\n // Sort the paths alphabetically\n const fileMetadataSorted = Array.from(fileMetadata).sort((a, b) =>\n a.path.localeCompare(b.path),\n );\n\n fileMetadataSorted.forEach((metadata) => {\n const path = metadata.path;\n /**\n * If the path ends with a /, the last array item will be an empty string.\n * The filter removes these falsy values.\n */\n const splitPath = path.split('/').filter(Boolean);\n const endsWithSlash = path.at(-1) === '/';\n\n splitPath.reduce((r: Output, label, index) => {\n if (!r.cache[label]) {\n // Create a cache of the files and folders added to our tree\n r.cache[label] = {\n tree: [],\n cache: {},\n };\n const fileName = splitPath.at(-1);\n\n const isFile = label === fileName && index === splitPath.length - 1 && !endsWithSlash;\n\n // Add to the tree\n r.tree.push({\n label,\n children: r.cache[label].tree,\n ...(isFile\n ? { metadata, type: 'file' }\n : {\n type: 'folder',\n dirname: splitPath.slice(0, index + 1).join('/'),\n }),\n });\n }\n\n return r.cache[label];\n }, output);\n });\n\n const folders = output.tree.filter((item: TreeNode) => item.type === 'folder');\n const files = output.tree.filter((item) => item.type === 'file');\n\n return [...folders, ...files];\n}\n","/**\n * The path argument should be relative to the project root and contain the filename.\n * For example: some/folder/here/file.js\n */\nexport function createFormDataBlob(path: string, content = '', type = 'text/plain') {\n const blobManifest = new Blob([content], { type });\n const formData = new FormData();\n formData.append('file', blobManifest, path);\n return formData;\n}\n","import { acceptHMRUpdate, defineStore } from 'pinia';\nimport { plainToInstance } from 'class-transformer';\nimport path from '~/utilities/path';\nimport pyscriptApi from '~/utilities/pyscript-api';\nimport type { PsdcApiError } from '~/utilities/pyscript-api-models';\nimport { FileMetadata } from '~/utilities/pyscript-api-models';\nimport { buildFileTree } from '~/utilities/build-file-tree';\nimport type { TreeNode } from '~/utilities/build-file-tree';\nimport { createFormDataBlob } from '~/utilities/create-form-data-blob';\nimport { FILE_IGNORE_LIST, MAX_FILE_SIZE, MAX_FILE_SIZE_TEXT } from '~/utilities/constants';\n\nexport interface FileContent {\n content: string;\n contentType: string;\n}\n\n/**\n * Virtual File System Store.\n *\n * Everything in this store revolves around a single project. If the state's\n * `projectId` changes, the files and all the store's data change as well.\n *\n * IMPORTANT NOTE:\n * When creating/renaming/deleting files or folders in S3 via the PyScript API\n * the calls are not awaited as that can cause a lag to the user.\n * To address this lag, the file is deleted from the store's state first, with\n * the assumption that the API call will succeed. However, if the API does fail,\n * the original file is re-added in the store's state.\n */\nexport const useVfsStore = defineStore('vfs', {\n state: () => ({\n /** Determines if we're loading the project's file list data. */\n loading: false,\n\n /**\n * Used for all PSDC API requests to determine which project to CRUD files\n * and folders to.\n */\n projectId: '',\n\n /** Project version */\n version: '',\n\n /**\n * A Map of all the files contained in a project.\n */\n fileListMap: new Map<FileMetadata['path'], FileMetadata>(),\n\n /**\n * Store the contents of files content. It represents the file's that are\n * opened in an editor tab. If a key is in the editorTabs, it should also\n * be here. The value may be undefined if the file is still loading.\n *\n * The key represents the path relative to the project's version path, not\n * the full S3 bucket path.\n * E.g. `index.html` instead of `user-id/project-id/version/index.html`.\n */\n fileContentMap: new Map<string, FileContent | null>(),\n\n /** Keeps track of files with unsaved changes */\n unsavedFilesMap: new Map<string, boolean>(),\n\n /**\n * List of relative file paths used to determine the tabs and their order\n * in the editor pane. Keys in this array should always be present in /\n * `state.fileContentMap`.\n *\n * The reason this state is necessary is two-fold. 1) Since tabs can be\n * reordered, we need to keep track of that order of that order. 2) When we\n * add the ability to have multiple panes, it's possible for the same file\n * to be open in multiple panes.\n */\n editorTabs: [] as string[],\n\n /** File path of the active file. The path is relative to the project's version. */\n activeFilePath: '',\n\n newFileField: {\n /**\n * Should not begin with a slash. An empty string would display the\n * new file field at the project's file explorer root (i.e. relative\n * to the version directory).\n *\n * For example, `path/to/some/dir` would be the directory where the\n * new file field appears.\n */\n dirname: '',\n show: false,\n },\n\n newFolderField: {\n /**\n * Should not begin with a slash. An empty string would display the\n * new folder field at the project's file explorer root (i.e. relative\n * to the version directory).\n *\n * For example, `path/to/some/dir` would be the directory where the\n * new directory field appears.\n */\n dirname: '',\n show: false,\n },\n\n /**\n * Updated when a file or folder is in a renaming state.\n */\n isRenaming: false,\n\n /**\n * Used to determine if we should create the .zip file to export the\n * project so it can be used as a library.\n */\n export: false,\n }),\n\n getters: {\n /**\n * Creates a complete tree of subfolders and files.\n */\n fileTree(): TreeNode[] {\n const fileListArray = Array.from(this.fileListMap, ([_path, metadata]) => metadata);\n return buildFileTree(fileListArray);\n },\n\n /**\n * @example {userId}/{projectId}/{version}/\n */\n bucketBasePath(): string {\n if (this.fileListMap.size === 0) return '';\n\n const { userId, projectId, version } = this.fileListMap.values().next().value;\n return `${userId}/${projectId}/${version}/`;\n },\n\n /**\n * Used to improve the UX when editing a file/folder name by adding an\n * opacity to all other file/folders in the tree. This can occur when\n * creating or renaming files and folders.\n */\n isEditingFileOrFolderName(): boolean {\n return this.newFileField.show || this.newFolderField.show || this.isRenaming;\n },\n },\n\n actions: {\n /** Initialize the state. */\n async initVfs(projectId: string, version?: string) {\n this.projectId = projectId;\n this.version = version || 'latest';\n\n this.loading = true;\n // Fetch our list of files and convert it to a Map\n await this.fetchFileList();\n\n // Check if we should export this project\n await this.shouldExportProject();\n this.loading = false;\n },\n\n /** Fetch our list of files and convert it to a Map */\n async fetchFileList() {\n const list = await pyscriptApi.listFiles(this.projectId, this.version);\n\n if (!list.length) return;\n\n const mappedArray: [string, FileMetadata][] = list.map((item) => [item.path, item]);\n this.fileListMap = new Map(mappedArray);\n },\n\n async shouldExportProject() {\n // Get the configuration file from fileListMap, this can\n // be a toml or json file.\n const project = await pyscriptApi.getProject(this.projectId);\n this.export = project.export;\n },\n\n /** Reset the state. */\n resetVfs() {\n this.$reset();\n },\n\n /**\n * Checks if a file or folder path has a conflict with an existing file or folder.\n * E.g. Creating a folder `path/to/some/.vscode/` conflicts with the file `path/to/some/.vscode`.\n * E.g. Renaming a folder `path/to/old-folder/` to `path/to/new-folder/` conflicts if a file or\n * folder named `path/to/new-folder/`.\n *\n * @param path Path of the file or folder. E.g. `path/to/some/file.txt` or `path/to/folder/`.\n */\n hasConflict(path: string): boolean {\n const isFolder = path.endsWith('/');\n const allPaths = Array.from(this.fileListMap.keys(), this.convertPathToString);\n const pathString = this.convertPathToString(path);\n return allPaths.some((p) => (isFolder ? p.startsWith(pathString) : p === pathString));\n },\n\n /**\n * Creates restore point in case the API request fails.\n *\n * @returns\n * A function that restores the state to the point when the function was called.\n */\n createRestorePoint() {\n const backup = {\n fileListMap: new Map(this.fileListMap),\n fileContentMap: new Map(this.fileContentMap),\n unsavedFilesMap: new Map(this.unsavedFilesMap),\n editorTabs: [...this.editorTabs],\n activeFilePath: this.activeFilePath,\n };\n\n return () => {\n this.fileListMap = backup.fileListMap;\n this.fileContentMap = backup.fileContentMap;\n this.unsavedFilesMap = backup.unsavedFilesMap;\n this.editorTabs = backup.editorTabs;\n this.activeFilePath = backup.activeFilePath;\n };\n },\n\n /**\n * Converts a path to a string.\n *\n * Use this to compare pahts. DON'T USE something like `.startsWith()` as\n * that can lead to false positives.\n *\n * @param filePath Path of the file to create. E.g. `path/to/some/file.txt`.\n * @returns A string representation of the path. E.g. `path,to,some,file.txt`.\n */\n convertPathToString(filePath: string) {\n return filePath.split('/').toString();\n },\n\n /**\n * Closes a tab in the editor pane by removing it from the store state's\n * `fileContentMap`, `unsavedFilesMap`, `editorTabs`, and `activeFilePath`.\n *\n * @param path Path of the file to close. E.g. `path/to/some/file.txt`.\n */\n closeTab(path: string) {\n const index = this.editorTabs.indexOf(path);\n if (index > -1) {\n this.editorTabs.splice(index, 1);\n }\n\n this.fileContentMap.delete(path);\n this.unsavedFilesMap.delete(path);\n\n if (this.activeFilePath === path) {\n this.activeFilePath = '';\n }\n },\n\n /**\n * Opens a tab in the editor pane by adding it to the store state's\n * `fileContentMap`, `editorTabs`, and `activeFilePath`.\n *\n * @param path Path of the file to close. E.g. `path/to/some/file.txt`.\n */\n async openTab(path: string) {\n if (!path || !this.fileListMap.has(path)) return;\n // Set the active file path.\n this.activeFilePath = path;\n\n // Open the tab if it's not already open.\n if (!this.editorTabs.includes(path)) {\n this.editorTabs.push(path);\n }\n\n // Fetch the file content if it's not already loaded.\n if (!this.fileContentMap.has(path)) {\n // Set the path to null to indicate it's loading.\n this.fileContentMap.set(path, null);\n await this.fetchFileContent(path);\n }\n },\n\n /** Updates a tab path. Used for when a rename or move occurs. */\n patchTab(oldPath: string, newPath: string) {\n const index = this.editorTabs.indexOf(oldPath);\n\n if (index > -1) {\n this.editorTabs[index] = newPath;\n }\n },\n\n /**\n * Updates a file's path and metadata in the store's state.\n *\n * This can occur when renaming, replacing, or moving files.\n */\n patchFileListPathAndMetadata(oldPath: string, newPath: string, fileMetadata: FileMetadata) {\n if (!this.fileListMap.has(oldPath)) {\n throw new Error(`The file \"${oldPath}\" does not exist.`);\n }\n\n this.fileListMap.set(newPath, fileMetadata);\n this.fileListMap.delete(oldPath);\n },\n\n /**\n * Updates a file's path and content and metadata in the store's state.\n *\n * This can occur when renaming, replacing, or moving files.\n */\n patchFileContentPath(oldPath: string, newPath: string) {\n // Only need to update the file content if it was already previously loaded.\n const oldFileContent = this.fileContentMap.get(oldPath);\n if (oldFileContent) {\n this.fileContentMap.set(newPath, oldFileContent);\n this.fileContentMap.delete(oldPath);\n }\n },\n\n /**\n * When the active file's path to the new path when a file is renamed or moved.\n */\n patchActiveFilePath(oldPath: string, newPath: string) {\n // Update the active file path if it was previously active.\n if (this.activeFilePath === oldPath) {\n this.activeFilePath = newPath;\n }\n },\n\n /**\n * When the active file's path to the new path only when it's renamed. This\n * doesn't occur when a file is moved because the file is saved prior to\n * it moving.\n */\n patchUnsavedFilesMap(oldPath: string, newPath: string) {\n const value = this.unsavedFilesMap.get(oldPath);\n if (value) {\n this.unsavedFilesMap.set(newPath, value);\n this.unsavedFilesMap.delete(oldPath);\n }\n },\n\n /**\n * Adds a file path to the touched file paths, used to detect if a file has\n * unsaved changes.\n */\n addTouchedFilePath(path: string) {\n this.unsavedFilesMap.set(path, true);\n },\n\n /**\n * Fetches the content of a file and stores it.\n *\n * @param path Path of the file to fetch.\n */\n async fetchFileContent(path: string) {\n if (!path) throw new Error('The path is required to fetch a file.');\n\n // We've already fetched this file before so no need to refetch\n if (this.fileContentMap.get(path)?.content) {\n return this.fileContentMap.get(path)?.content;\n }\n\n const resp = await pyscriptApi.getFileContent(this.projectId, this.version, path);\n const contentType = resp.headers.get('content-type');\n let content: string | undefined;\n\n if (!resp.ok) {\n throw new Error(`Failed to fetch file content for \"${path}\".`);\n }\n\n if (\n path.endsWith('.webp') ||\n contentType?.includes('image/') ||\n contentType?.includes('video/') ||\n contentType?.includes('audio/') ||\n contentType === 'application/pdf'\n ) {\n content = await resp.blob().then((blob) => {\n return new Promise<string>((resolve, reject) => {\n try {\n const reader = new FileReader();\n reader.onload = function () {\n resolve(this.result as string);\n };\n reader.readAsDataURL(blob);\n } catch (e) {\n reject(e);\n }\n });\n });\n } else {\n content = await resp.text();\n }\n\n this.fileContentMap.set(path, {\n content,\n contentType,\n } as FileContent);\n\n return content;\n },\n\n /**\n * Downloads a file.\n *\n * @param filePath Path of the file. E.g. `path/to/some/file.txt`\n */\n async downloadFile(filePath: string) {\n const fileName = path.basename(filePath);\n const downloadUrl = this.fileListMap.get(filePath)?.url;\n\n if (!filePath || !downloadUrl) {\n // eslint-disable-next-line no-alert\n alert('Failed to download');\n return;\n }\n\n // eslint-disable-next-line no-alert\n const confirmResult = confirm(`Are you sure you want to download \"${fileName}\"?`);\n if (!confirmResult) {\n return;\n }\n\n const resp = await pyscriptApi.getFileContent(this.projectId, this.version, filePath);\n const blob = await resp.blob();\n\n // Create a link in memory and use it to download the file.\n const link = document.createElement('a');\n link.href = URL.createObjectURL(blob);\n link.download = fileName;\n link.click();\n },\n\n /**\n * Duplicates a file.\n *\n * @param filePath Path of the file. E.g. `path/to/some/file.txt`\n */\n async duplicateFile(filePath: string) {\n // Get the full path of the file.\n const fullPath = this.fileListMap.get(filePath)!.path;\n\n // Fetch the blob contents of a file, which can be an image, video, audio, text, etc.\n const resp = await pyscriptApi.getFileContent(this.projectId, this.version, fullPath);\n const contentType = resp.headers.get('content-type');\n const blob = await resp.blob();\n\n if (!blob && blob !== '') {\n throw new Error(`Failed to duplicate the file \"${filePath}\".`);\n }\n\n // Update the path to append `-copy` to the file's name.\n const parsedFilePath = path.parse(filePath);\n const newFilePath = path.join(\n parsedFilePath.dir,\n `${parsedFilePath.name}-copy${parsedFilePath.ext}`,\n );\n\n // Create a File object.\n const file = new File([blob], newFilePath, { type: contentType! });\n\n // Upload the file.\n this.uploadFile(file);\n },\n\n /**\n * Creates a new empty file.\n *\n * @param filePath Path of the file to create. E.g. `path/to/some/file.txt`.\n */\n async createFile(filePath: string): Promise<FileMetadata> {\n // Short circuit if the new file path already exists as a file or folder.\n if (this.hasConflict(filePath)) {\n const error: PsdcApiError = {\n code: 'CONFLICT',\n message: `A file or folder \"${filePath}\" already exists at this location. Please choose a different name.`,\n status: 409,\n };\n throw error;\n }\n\n const formData = createFormDataBlob(filePath, '');\n const uploadedFileMetadata = await pyscriptApi.uploadFile(this.projectId, formData);\n this.fileListMap.set(filePath, uploadedFileMetadata);\n return uploadedFileMetadata;\n },\n\n /**\n * Creates a new empty directory.\n *\n * NOTE: Don't pass a trailing slash.\n * @param folderPath Name of the folder to create. E.g. `path/to/some/folder`.\n */\n async createFolder(folderPath: string): Promise<FileMetadata> {\n // Short circuit if the new file path already exists as a file or folder.\n if (this.hasConflict(`${folderPath}/`) || this.hasConflict(folderPath)) {\n const error: PsdcApiError = {\n code: 'CONFLICT',\n message: `A file or folder \"${folderPath}\" already exists at this location. Please choose a different name.`,\n status: 409,\n };\n throw error;\n }\n\n const uploadedFileMetadata = await pyscriptApi.createFolder(this.projectId, folderPath);\n this.fileListMap.set(uploadedFileMetadata.path, uploadedFileMetadata);\n return uploadedFileMetadata;\n },\n\n /**\n * Deletes a file in our store's state, then from S3 via PSDC API.\n * If the API request fails, the file is re-added to the store's state.\n *\n * @param filePathToDelete Path of the file to delete. E.g. `path/to/file.txt`.\n */\n async deleteFile(filePathToDelete: string) {\n /* Optimistically update the UI by removing the file from the state. */\n const restore = this.createRestorePoint();\n\n // Delete the path from `fileContentMap`, `unsavedFilesMap`, `editorTabs`, and `activeFilePath`.\n this.closeTab(filePathToDelete);\n\n // Delete the path from `fileListMap`.\n this.fileListMap.delete(filePathToDelete);\n\n /* Make the API call to delete the file from S3. If an error occurs, restore the state. */\n return pyscriptApi.deleteFile(this.projectId, filePathToDelete).catch((err) => {\n restore();\n\n // Throw the error to be handled by the caller.\n throw err;\n });\n },\n\n /**\n * Deletes a folder and all of its contents, as well as removing its files\n * from our state.\n *\n * @param folderPath Path of the folder to delete. E.g. `path/to/folder/`.\n */\n async deleteFolder(folderPath: string) {\n /* Optimistically update the UI. */\n const restore = this.createRestorePoint();\n const folderToDeleteAsString = this.convertPathToString(folderPath);\n\n // Iterate through all the files in the store's state to detect which\n // files are in the folder being deleted. If detected, delete the file\n // from all Map objects in the store's state.\n Array.from(this.fileListMap).forEach(([keyPath, _valueMetadata]) => {\n const keyPathAsString = this.convertPathToString(keyPath);\n\n // If the `keyPath` starts with the folder path being deleted, delete it.\n if (keyPathAsString.startsWith(folderToDeleteAsString)) {\n // 1. Delete the file from `fileListMap`.\n this.fileListMap.delete(keyPath);\n\n // 2. Delete the file from `fileContentMap`.\n this.fileContentMap.delete(keyPath);\n\n // 3. Delete the file from `unsavedFilesMap`.\n this.unsavedFilesMap.delete(keyPath);\n }\n });\n\n // 4. If the active file is in the deleted folder, remove it.\n if (this.activeFilePath.startsWith(folderPath)) {\n this.activeFilePath = '';\n }\n\n // 5. Remove open tabs that are in the deleted folder.\n this.editorTabs = this.editorTabs.filter((filePath) => !filePath.startsWith(folderPath));\n\n return pyscriptApi.deleteFolder(this.projectId, folderPath).catch((err) => {\n // If an error occours, restore the state.\n restore();\n\n // Throw the error to be handled by the caller.\n throw err;\n });\n },\n\n /**\n * Moves a file from one location to another.\n *\n * @param pathFrom Path of the file to move. E.g. `old/path/to/file.txt`\n * @param pathTo Path of the destination. E.g. `new/path/file.txt`\n * @param skipOverwriteConfirmation If true, skips the overwrite confirmation prompt.\n */\n async moveFile(pathFrom: string, pathTo: string, skipOverwriteConfirmation = false) {\n // Short circuit if the paths are the same\n if (pathFrom === pathTo) return;\n\n let shouldOverwrite = skipOverwriteConfirmation;\n const hasFile = this.fileListMap.has(pathTo);\n\n // TODO: Show this prompt in a toast message or custom confirmation modal.\n // Confirm if user wants to overwrite the file if it already exists.\n if (hasFile && !skipOverwriteConfirmation) {\n const fileName = path.basename(pathTo);\n\n // eslint-disable-next-line no-alert\n shouldOverwrite = confirm(\n `\"${fileName}\" already exists in this destination. Do you want to overwrite it?`,\n );\n\n if (!shouldOverwrite) {\n return;\n }\n }\n\n const pathFromMetadata = this.fileListMap.get(pathFrom);\n\n if (!pathFromMetadata) {\n throw new Error(`The file \"${pathFrom}\" does not exist.`);\n }\n\n // 1) Set as a ghost file to indicate to the user it's being moved.\n pathFromMetadata.isGhostFile = true;\n\n // 2) Fetch the file contents from S3 if not already loaded.\n const resp = await pyscriptApi.getFileContent(this.projectId, this.version, pathFrom);\n const blob = await resp.blob();\n\n // 3) Upload the file to the new location.const formData = new FormData();\n const formData = new FormData();\n formData.append('file', blob, pathTo);\n const uploadedFileMetadata =\n shouldOverwrite && hasFile\n ? await pyscriptApi.updateFile(this.projectId, formData)\n : await pyscriptApi.uploadFile(this.projectId, formData);\n\n // 4) Delete the file from the old location.\n await pyscriptApi.deleteFile(this.projectId, pathFrom);\n\n // 5) Update the store's state.\n this.patchFileListPathAndMetadata(pathFrom, pathTo, uploadedFileMetadata);\n this.patchTab(pathFrom, pathTo);\n this.patchFileContentPath(pathFrom, pathTo);\n this.patchActiveFilePath(pathFrom, pathTo);\n this.patchUnsavedFilesMap(pathFrom, pathTo);\n },\n\n /**\n * Moves a folder from one location to another.\n *\n * The paths should never start with a slash and should always end with a slash.\n * If it's the root directory, the path will be an empty string.\n *\n * @param pathFrom Path of the folder to move. E.g. `old/path/cool-folder/`\n * @param pathTo Path of the destination. E.g. `some/new/path/`\n */\n async moveFolder(pathFrom: string, pathTo: string) {\n // Short circuit if the paths are the same\n if (pathFrom === pathTo) return;\n\n // Short circuit if the `pathFrom` is being dropped into its parent.\n // E.g. `some/new/path/cool-folder/` into `some/new/path/`.\n let pathFromParentPath = path.dirname(pathFrom.slice(0, -1));\n pathFromParentPath = pathFromParentPath === '' ? '' : `${pathFromParentPath}/`;\n if (pathFromParentPath === pathTo) return;\n\n const folderName = pathFrom.split('/').filter(Boolean).at(-1) || '';\n const fileListArray = Array.from(this.fileListMap, ([_key, value]) => value);\n\n // Destination path including the folder name. E.g. `some/new/path/cool-folder`\n const pathToFolder = path.join(pathTo, folderName);\n\n const hasFolderConflict = fileListArray.some((file) => file.path.startsWith(pathToFolder));\n\n let confirmResult: boolean;\n\n if (hasFolderConflict) {\n // eslint-disable-next-line no-alert\n confirmResult = confirm(\n `A folder named \"${folderName}\" already exists at this destination. Do you want to merge the two?`,\n );\n\n if (!confirmResult) {\n return;\n }\n }\n\n // Get all the file paths in the folder being moved.\n const pathsInFolder = fileListArray\n .filter((file) => file.path.startsWith(pathFrom))\n .map((file) => file.path);\n\n pathsInFolder.forEach(async (filePathFrom) => {\n // Construct the destination path for each file/folder in the folder being moved.\n const newPathTo = path.join(pathToFolder, filePathFrom.replace(pathFrom, ''));\n\n if (filePathFrom.endsWith('/')) {\n // Create the folder in the new location by creating a file with a slash at the end.\n this.moveFile(filePathFrom, `${newPathTo}/`, confirmResult);\n } else {\n this.moveFile(filePathFrom, newPathTo, confirmResult);\n }\n });\n },\n\n /**\n * Rename a file in the store state and S3 via PSDC API.\n *\n * @param oldPath Path of the folder to rename. E.g. `path/to/old-name.txt`\n * @param newPath Path of the destination. E.g. `path/to/new-name.txt`\n */\n async renameFile(oldPath: string, newPath: string) {\n // Short circuit if the new file path already exists.\n if (this.hasConflict(newPath)) {\n const error: PsdcApiError = {\n code: 'CONFLICT',\n message: `A file or folder \"${newPath}\" already exists at this location. Please choose a different name.`,\n status: 409,\n };\n throw error;\n }\n\n /* Optimistically update the UI. */\n\n const restore = this.createRestorePoint();\n\n // Update the metadata with the new path.\n // Doing this optimistically prior to the API request to prevent a lag and give the feeling of a faster UI.\n const newBasename = path.basename(newPath);\n const oldMetadata = this.fileListMap.get(oldPath) as FileMetadata;\n const newMetadata = plainToInstance(FileMetadata, {\n ...oldMetadata,\n path: path.join(oldMetadata.parsedPath.dir, newBasename), // Full S3 bucket path\n name: newBasename,\n });\n\n // Update the store's state.\n this.patchFileListPathAndMetadata(oldPath, newPath, newMetadata);\n this.patchTab(oldPath, newPath);\n this.patchFileContentPath(oldPath, newPath);\n this.patchActiveFilePath(oldPath, newPath);\n\n // PSDC API request to rename the file in S3\n // Intentionally not awaiting to not cause a lag.\n return pyscriptApi.renameFileOrFolder(this.projectId, oldPath, newPath).catch((err) => {\n // If an error occours, restore the state.\n restore();\n\n // Throw the error to be handled by the caller.\n throw err;\n });\n },\n\n /**\n * Rename a folder in the store state and S3 via PSDC API.\n *\n * @param oldPath Path of the folder to rename. E.g. `path/to/old-folder/`\n * @param newPath Path of the destination. E.g. `path/to/new-folder/`\n */\n async renameFolder(oldPath: string, newPath: string) {\n // Short circuit if no changes were made.\n if (oldPath === newPath) return;\n\n // Throw error if the new file path already exists.\n if (this.hasConflict(newPath)) {\n const error: PsdcApiError = {\n code: 'CONFLICT',\n message: `A folder already exists at \"${newPath}\".`,\n status: 409,\n };\n throw error;\n }\n\n /* Optimistically update the UI. */\n\n // Create restore point in case the API request fails.\n const restore = this.createRestorePoint();\n const oldPathAsString = this.convertPathToString(oldPath);\n\n // NOTE: Don't use Map.prototype.forEach() here or else we'll end up in\n // an infinite loop due to setting and deleting keys in the same loop.\n Array.from(this.fileListMap).forEach(([keyPath, valueMetadata]) => {\n const keyPathAsString = this.convertPathToString(keyPath);\n\n // Replace the path in all our state's Map objects with the new path.\n // E.g. `some/path/old-folder/index.html` -> `some/path/new-folder/index.html`\n if (keyPathAsString.startsWith(oldPathAsString)) {\n const newFilePath = keyPath.replace(oldPath, newPath);\n\n // 1. Update the keys of `fileListMap` that contain the old path.\n const newMetadata = plainToInstance(FileMetadata, {\n ...valueMetadata,\n path: valueMetadata.path.replace(oldPath, newPath), // Update the full S3 bucket path.\n });\n this.fileListMap.set(newFilePath, newMetadata);\n this.fileListMap.delete(keyPath);\n\n // 2. Update the keys of `fileContentMap` that contain the old path.\n if (this.fileContentMap.has(keyPath)) {\n this.fileContentMap.set(newFilePath, this.fileContentMap.get(keyPath) as FileContent);\n this.fileContentMap.delete(keyPath);\n }\n\n // 3. Update the keys of `unsavedFilesMap` that contain the old path.\n if (this.unsavedFilesMap.has(keyPath)) {\n this.unsavedFilesMap.set(newFilePath, this.unsavedFilesMap.get(keyPath) as boolean);\n this.unsavedFilesMap.delete(keyPath);\n }\n }\n });\n\n // 4. If the active file is in the renamed folder, update its path.\n if (this.activeFilePath.startsWith(oldPath)) {\n this.activeFilePath = this.activeFilePath.replace(oldPath, newPath);\n }\n\n // 5. Update all open tabs that contain the old folder path.\n this.editorTabs = this.editorTabs.map((filePath) =>\n filePath.startsWith(oldPath) ? filePath.replace(oldPath, newPath) : filePath,\n );\n\n const oldPathWithoutTrailingSlash = oldPath.slice(0, -1);\n const newPathWithoutTrailingSlash = newPath.slice(0, -1);\n\n return pyscriptApi\n .renameFileOrFolder(\n this.projectId,\n oldPathWithoutTrailingSlash,\n newPathWithoutTrailingSlash,\n )\n .catch((err) => {\n // If an error occours, restore the state.\n restore();\n\n // Throw the error to be handled by the caller.\n throw err;\n });\n },\n\n /**\n * Handles files to be uploaded to S3 via PSDC API.\n * This may occur via drag-andpdrop or the file input.\n *\n * The `File.name` should be the path without a leading slash.\n * E.g. `path/to/file.txt`\n */\n async uploadFile(file: File) {\n const basename = path.basename(file.name);\n\n if (file.size > MAX_FILE_SIZE) {\n // eslint-disable-next-line no-alert\n alert(`UPLOAD ERROR: ${file.name} exceeds max size (${MAX_FILE_SIZE_TEXT})`);\n return;\n }\n\n if (FILE_IGNORE_LIST.includes(basename)) {\n return;\n }\n\n // Create a temporary file metadata that will be replaced once the file is uploaded.\n const tempFileMetadata = plainToInstance(FileMetadata, {\n hash: '',\n // Need to add a fake path to accurately display the file in the tree.\n path: file.name,\n name: basename,\n size: file.size,\n updated_at: '',\n // A flag to indicate the file is in the process of being uploaded.\n isGhostFile: true,\n });\n\n const hasFile = this.fileListMap.has(file.name);\n let shouldOverwrite = false;\n\n // Confirm if user wants to overwrite the file if it already exists.\n if (hasFile) {\n // eslint-disable-next-line no-alert\n shouldOverwrite = confirm(\n `\"${file.name}\" already exists in this destination. Do you want to overwrite it?`,\n );\n\n if (!shouldOverwrite) {\n return;\n }\n }\n\n // Create a ghost file in the file tree while the file is uploading.\n this.fileListMap.set(file.name, tempFileMetadata);\n\n // Create the form data.\n const formData = new FormData();\n formData.append('file', file, file.name);\n\n // Upload the file, then replace the ghost file with the file metadata.\n const fileMetadata =\n hasFile && shouldOverwrite\n ? await pyscriptApi.updateFile(this.projectId, formData)\n : await pyscriptApi.uploadFile(this.projectId, formData);\n\n this.fileListMap.set(file.name, fileMetadata);\n\n // If the file was already opened in a tab, replace it with the uploaded file's content.\n if (this.fileContentMap.has(file.name)) {\n this.fileContentMap.delete(file.name);\n this.fetchFileContent(file.name);\n }\n },\n\n /**\n * Used to determine at which directory in our tree we should display the\n * input field for the user to enter the new file's name before it's created\n * and uploaded to S3.\n */\n updateNewFileField({ dirname, show }: { dirname: string; show: boolean }): void {\n if (typeof dirname === 'string') {\n this.newFileField.dirname = dirname;\n }\n\n if (typeof show === 'boolean') {\n this.newFileField.show = show;\n }\n },\n\n /**\n * Used to determine at which directory in our tree we should display the\n * input field for the user to enter the new folders's name before it's\n * created and uploaded to S3.\n */\n updateNewFolderField({ dirname, show }: { dirname: string; show: boolean }): void {\n if (typeof dirname === 'string') {\n this.newFolderField.dirname = dirname;\n }\n\n if (typeof show === 'boolean') {\n this.newFolderField.show = show;\n }\n },\n },\n});\n\nif (import.meta.hot) import.meta.hot.accept(acceptHMRUpdate(useVfsStore, import.meta.hot));\n","<script setup lang=\"ts\">\nimport { computed, defineAsyncComponent, onBeforeMount, reactive } from 'vue';\nimport { onBeforeRouteUpdate } from 'vue-router';\nimport { useHead } from '@unhead/vue';\nimport { useEventListener } from '@vueuse/core';\nimport { show404Page } from '~/utilities/show-404-page';\nimport { useProjectStore } from '~/stores/project-store';\nimport { useVfsStore } from '~/stores/vfs';\nimport Spinner from '~/components/Spinner.vue';\n\n// Populated by the Vue Router\ninterface Props {\n usernameOrUserId: string;\n projectSlugOrId: string;\n version: string;\n}\n\nconst props = defineProps<Props>();\nconst projectStore = useProjectStore();\nconst vfsStore = useVfsStore();\n\nconst ProjectPageEditable = defineAsyncComponent(\n () => import('~/views/projects/components/ProjectPageEditable.vue'),\n);\n\nconst ProjectPageReadOnly = defineAsyncComponent(\n () => import('~/views/projects/components/ProjectPageReadOnly.vue'),\n);\n\nconst ProjectPageInvent = defineAsyncComponent(\n () => import('~/views/projects/components/ProjectPageInvent.vue'),\n);\n\nconst NotFoundView = defineAsyncComponent(() => import('~/views/NotFoundView.vue'));\n\nasync function pageTitle() {\n if (projectStore.project) {\n const verb = projectStore.isProjectOwner ? 'Edit' : 'View';\n return `${verb} - ${projectStore.project?.name}`;\n }\n\n return '';\n}\n\n// Set the version data specifically for the current route\nconst versionData = computed(() => projectStore.versions.find((v) => v.version === props.version));\n\nuseHead({ title: pageTitle() });\n\nconst state = reactive({\n loading: true,\n});\n\nuseEventListener(window, 'dragover', (e) => e.preventDefault());\nuseEventListener(window, 'drop', (e) => e.preventDefault());\n\nonBeforeMount(async () => {\n try {\n // Reset the stores if not the same project previously loaded.\n if (\n projectStore.project?.slug !== props.projectSlugOrId ||\n projectStore.project?.id !== props.projectSlugOrId\n ) {\n resetStores();\n }\n\n state.loading = true;\n\n // Setup the store with data for the project and its versions\n await projectStore.fetchProjectAndVersions(props.usernameOrUserId, props.projectSlugOrId);\n } catch (error) {\n show404Page();\n } finally {\n state.loading = false;\n }\n});\n\n/**\n * A route update occurs when the route changes, but the component stays mounted.\n * This may occur when we're renaming a project, creating a new project, or\n * forking a project while already on the project page\n */\nonBeforeRouteUpdate(async (to, from) => {\n // Don't need to refetch the data if the users was just on this page\n if (to.params.projectSlugOrId !== from.params.projectSlugOrId) {\n // Reset the stores if not the same project previously loaded.\n if (\n projectStore.project?.slug !== props.projectSlugOrId ||\n projectStore.project?.id !== props.projectSlugOrId\n ) {\n resetStores();\n\n try {\n state.loading = true;\n\n await projectStore.fetchProjectAndVersions(\n to.params.usernameOrUserId as string,\n to.params.projectSlugOrId as string,\n );\n } catch (error) {\n show404Page();\n } finally {\n state.loading = false;\n }\n }\n }\n});\n\n/**\n * Completely reset the store's data.\n */\nfunction resetStores() {\n projectStore.$reset();\n vfsStore.$reset();\n}\n</script>\n\n<template>\n <div v-if=\"state.loading\" class=\"flex h-full items-center justify-center text-gray-600\">\n <Spinner class=\"h-8 w-8\" />\n </div>\n\n <ProjectPageInvent\n v-else-if=\"\n projectStore.project?.type === 'app/invent' &&\n projectStore.isProjectOwner &&\n versionData &&\n $route.query.code !== '1'\n \"\n :project-data=\"projectStore.project\"\n :version-data=\"versionData\"\n />\n\n <ProjectPageEditable\n v-else-if=\"projectStore.project && projectStore.isProjectOwner && versionData\"\n :project-data=\"projectStore.project\"\n :version-data=\"versionData\"\n />\n\n <ProjectPageReadOnly\n v-else-if=\"projectStore.project && versionData?.published\"\n :project-data=\"projectStore.project\"\n :version-data=\"versionData\"\n />\n\n <NotFoundView v-else />\n</template>\n"],"file":"assets/ProjectView-B1g_UpJj.js"}