{"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"}