Compare commits
2 Commits
0b368b09fa
...
268093d223
| Author | SHA1 | Date | |
|---|---|---|---|
| 268093d223 | |||
| c64a7b8d44 |
@@ -192,12 +192,23 @@ type CreateProjectFolderInput struct {
|
|||||||
Name string
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DeleteProjectFolderInput struct {
|
||||||
|
ProjectID string
|
||||||
|
FolderID string
|
||||||
|
}
|
||||||
|
|
||||||
type CreateProjectFolderResult struct {
|
type CreateProjectFolderResult struct {
|
||||||
ProjectID string `json:"projectId"`
|
ProjectID string `json:"projectId"`
|
||||||
CreatedFolder ProjectHierarchyFolderRecord `json:"createdFolder"`
|
CreatedFolder ProjectHierarchyFolderRecord `json:"createdFolder"`
|
||||||
Folders []ProjectHierarchyFolderRecord `json:"folders"`
|
Folders []ProjectHierarchyFolderRecord `json:"folders"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DeleteProjectFolderResult struct {
|
||||||
|
ProjectID string `json:"projectId"`
|
||||||
|
DeletedFolderID string `json:"deletedFolderId"`
|
||||||
|
Folders []ProjectHierarchyFolderRecord `json:"folders"`
|
||||||
|
}
|
||||||
|
|
||||||
type projectHierarchyFolderRow struct {
|
type projectHierarchyFolderRow struct {
|
||||||
Path string
|
Path string
|
||||||
ParentPath string
|
ParentPath string
|
||||||
@@ -898,6 +909,14 @@ func (service *Service) CreateProjectTreeFolder(ctx context.Context, input Creat
|
|||||||
return service.createProjectHierarchyFolder(ctx, input, projectTreeRootPath, service.createProjectTreeFolderOnDisk)
|
return service.createProjectHierarchyFolder(ctx, input, projectTreeRootPath, service.createProjectTreeFolderOnDisk)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (service *Service) DeleteProjectFolder(ctx context.Context, input DeleteProjectFolderInput) (DeleteProjectFolderResult, error) {
|
||||||
|
return service.deleteProjectHierarchyFolder(ctx, input, projectHierarchyRootPath, service.deleteProjectHierarchyFolderOnDisk)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *Service) DeleteProjectTreeFolder(ctx context.Context, input DeleteProjectFolderInput) (DeleteProjectFolderResult, error) {
|
||||||
|
return service.deleteProjectHierarchyFolder(ctx, input, projectTreeRootPath, service.deleteProjectTreeFolderOnDisk)
|
||||||
|
}
|
||||||
|
|
||||||
func (service *Service) createProjectHierarchyFolder(
|
func (service *Service) createProjectHierarchyFolder(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
input CreateProjectFolderInput,
|
input CreateProjectFolderInput,
|
||||||
@@ -935,6 +954,42 @@ func (service *Service) createProjectHierarchyFolder(
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (service *Service) deleteProjectHierarchyFolder(
|
||||||
|
ctx context.Context,
|
||||||
|
input DeleteProjectFolderInput,
|
||||||
|
rootPath func(projectSlug string) string,
|
||||||
|
deleteOnDisk func(projectSlug, folderID string) (string, error),
|
||||||
|
) (DeleteProjectFolderResult, error) {
|
||||||
|
project, err := service.loadProjectByID(ctx, strings.TrimSpace(input.ProjectID))
|
||||||
|
if err != nil {
|
||||||
|
return DeleteProjectFolderResult{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
deletedFolderID, err := deleteOnDisk(project.Slug, input.FolderID)
|
||||||
|
if err != nil {
|
||||||
|
return DeleteProjectFolderResult{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := posixproj.NewProjector(service.db, service.posixRoot).Rebuild(ctx); err != nil {
|
||||||
|
return DeleteProjectFolderResult{}, fmt.Errorf("rebuild POSIX projection: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
folders, err := service.getProjectHierarchyFoldersByRootPath(ctx, project.ID, rootPath)
|
||||||
|
if err != nil {
|
||||||
|
return DeleteProjectFolderResult{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, found := findProjectHierarchyFolder(folders, deletedFolderID); found {
|
||||||
|
return DeleteProjectFolderResult{}, fmt.Errorf("deleted project folder still present in projection")
|
||||||
|
}
|
||||||
|
|
||||||
|
return DeleteProjectFolderResult{
|
||||||
|
ProjectID: project.ID,
|
||||||
|
DeletedFolderID: deletedFolderID,
|
||||||
|
Folders: folders,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (service *Service) listWorkspaces(ctx context.Context) ([]WorkspaceRecord, error) {
|
func (service *Service) listWorkspaces(ctx context.Context) ([]WorkspaceRecord, error) {
|
||||||
rows, err := service.db.Pool.Query(ctx, `
|
rows, err := service.db.Pool.Query(ctx, `
|
||||||
SELECT id::text, organization_id::text, name, slug, kind::text, department_id::text, team_id::text, project_id::text
|
SELECT id::text, organization_id::text, name, slug, kind::text, department_id::text, team_id::text, project_id::text
|
||||||
@@ -1218,6 +1273,14 @@ func (service *Service) createProjectTreeFolderOnDisk(projectSlug, parentFolderI
|
|||||||
return service.createProjectFolderOnDisk(projectSlug, parentFolderID, name, projectTreeRootPath)
|
return service.createProjectFolderOnDisk(projectSlug, parentFolderID, name, projectTreeRootPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (service *Service) deleteProjectHierarchyFolderOnDisk(projectSlug, folderID string) (string, error) {
|
||||||
|
return service.deleteProjectFolderOnDisk(projectSlug, folderID, projectHierarchyRootPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *Service) deleteProjectTreeFolderOnDisk(projectSlug, folderID string) (string, error) {
|
||||||
|
return service.deleteProjectFolderOnDisk(projectSlug, folderID, projectTreeRootPath)
|
||||||
|
}
|
||||||
|
|
||||||
func (service *Service) createProjectFolderOnDisk(
|
func (service *Service) createProjectFolderOnDisk(
|
||||||
projectSlug, parentFolderID, name string,
|
projectSlug, parentFolderID, name string,
|
||||||
rootPathBuilder func(projectSlug string) string,
|
rootPathBuilder func(projectSlug string) string,
|
||||||
@@ -1294,6 +1357,45 @@ func (service *Service) createProjectFolderOnDisk(
|
|||||||
return filepath.ToSlash(filepath.Join(containerProjectionPath, folderName)), folderSlug, nil
|
return filepath.ToSlash(filepath.Join(containerProjectionPath, folderName)), folderSlug, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (service *Service) deleteProjectFolderOnDisk(
|
||||||
|
projectSlug, folderID string,
|
||||||
|
rootPathBuilder func(projectSlug string) string,
|
||||||
|
) (string, error) {
|
||||||
|
posixRoot := strings.TrimSpace(service.posixRoot)
|
||||||
|
if posixRoot == "" {
|
||||||
|
return "", fmt.Errorf("POSIX root is not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
trimmedFolderID := strings.TrimSpace(folderID)
|
||||||
|
if trimmedFolderID == "" {
|
||||||
|
return "", ErrProjectFolderNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProjectionPath := rootPathBuilder(projectSlug)
|
||||||
|
folderProjectionPath := strings.TrimPrefix(filepath.ToSlash(filepath.Clean("/"+trimmedFolderID)), "/")
|
||||||
|
if folderProjectionPath == "." || folderProjectionPath == rootProjectionPath || !strings.HasPrefix(folderProjectionPath, rootProjectionPath+"/") {
|
||||||
|
return "", ErrProjectFolderNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
folderDir := filepath.Join(posixRoot, filepath.FromSlash(folderProjectionPath))
|
||||||
|
info, err := os.Stat(folderDir)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return "", ErrProjectFolderNotFound
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("stat project folder: %w", err)
|
||||||
|
}
|
||||||
|
if !info.IsDir() {
|
||||||
|
return "", ErrProjectFolderNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.RemoveAll(folderDir); err != nil {
|
||||||
|
return "", fmt.Errorf("delete project folder: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return folderProjectionPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
func buildProjectHierarchyFolderTree(rows []projectHierarchyFolderRow, rootParentPath string) []ProjectHierarchyFolderRecord {
|
func buildProjectHierarchyFolderTree(rows []projectHierarchyFolderRow, rootParentPath string) []ProjectHierarchyFolderRecord {
|
||||||
if len(rows) == 0 {
|
if len(rows) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ type createProjectFolderRequest struct {
|
|||||||
ParentFolderID string `json:"parentFolderId"`
|
ParentFolderID string `json:"parentFolderId"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type deleteProjectFolderRequest struct {
|
||||||
|
FolderID string `json:"folderId"`
|
||||||
|
}
|
||||||
|
|
||||||
func (routes apiRoutes) handleProjectFolders(w http.ResponseWriter, r *http.Request) {
|
func (routes apiRoutes) handleProjectFolders(w http.ResponseWriter, r *http.Request) {
|
||||||
projectID := strings.TrimSpace(chi.URLParam(r, "projectId"))
|
projectID := strings.TrimSpace(chi.URLParam(r, "projectId"))
|
||||||
if projectID == "" {
|
if projectID == "" {
|
||||||
@@ -26,7 +30,7 @@ func (routes apiRoutes) handleProjectFolders(w http.ResponseWriter, r *http.Requ
|
|||||||
|
|
||||||
folders, err := routes.bootstrapService().GetProjectHierarchyFolders(r.Context(), projectID)
|
folders, err := routes.bootstrapService().GetProjectHierarchyFolders(r.Context(), projectID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
routes.writeProjectFolderError(w, r, err)
|
routes.writeProjectFolderError(w, r, err, "load")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +70,7 @@ func (routes apiRoutes) handleCreateProjectFolder(w http.ResponseWriter, r *http
|
|||||||
Name: payload.Name,
|
Name: payload.Name,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
routes.writeProjectFolderError(w, r, err)
|
routes.writeProjectFolderError(w, r, err, "persist")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +83,37 @@ func (routes apiRoutes) handleCreateProjectFolder(w http.ResponseWriter, r *http
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (routes apiRoutes) handleDeleteProjectFolder(w http.ResponseWriter, r *http.Request) {
|
||||||
|
projectID := strings.TrimSpace(chi.URLParam(r, "projectId"))
|
||||||
|
if projectID == "" {
|
||||||
|
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_request", "Project ID is required.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := decodeDeleteProjectFolderRequest(r)
|
||||||
|
if strings.TrimSpace(payload.FolderID) == "" {
|
||||||
|
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_request", "Folder ID is required.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := routes.bootstrapService().DeleteProjectFolder(r.Context(), bootstrapservice.DeleteProjectFolderInput{
|
||||||
|
ProjectID: projectID,
|
||||||
|
FolderID: payload.FolderID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
routes.writeProjectFolderError(w, r, err, "delete")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteJSON(w, http.StatusOK, map[string]any{
|
||||||
|
"data": result,
|
||||||
|
"meta": map[string]any{
|
||||||
|
"resource": "project-folder-delete",
|
||||||
|
"persisted": true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (routes apiRoutes) handleProjectTreeFolders(w http.ResponseWriter, r *http.Request) {
|
func (routes apiRoutes) handleProjectTreeFolders(w http.ResponseWriter, r *http.Request) {
|
||||||
projectID := strings.TrimSpace(chi.URLParam(r, "projectId"))
|
projectID := strings.TrimSpace(chi.URLParam(r, "projectId"))
|
||||||
if projectID == "" {
|
if projectID == "" {
|
||||||
@@ -88,7 +123,7 @@ func (routes apiRoutes) handleProjectTreeFolders(w http.ResponseWriter, r *http.
|
|||||||
|
|
||||||
folders, err := routes.bootstrapService().GetProjectTreeFolders(r.Context(), projectID)
|
folders, err := routes.bootstrapService().GetProjectTreeFolders(r.Context(), projectID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
routes.writeProjectFolderError(w, r, err)
|
routes.writeProjectFolderError(w, r, err, "load")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +163,7 @@ func (routes apiRoutes) handleCreateProjectTreeFolder(w http.ResponseWriter, r *
|
|||||||
Name: payload.Name,
|
Name: payload.Name,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
routes.writeProjectFolderError(w, r, err)
|
routes.writeProjectFolderError(w, r, err, "persist")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,17 +176,54 @@ func (routes apiRoutes) handleCreateProjectTreeFolder(w http.ResponseWriter, r *
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (routes apiRoutes) writeProjectFolderError(w http.ResponseWriter, r *http.Request, err error) {
|
func (routes apiRoutes) handleDeleteProjectTreeFolder(w http.ResponseWriter, r *http.Request) {
|
||||||
|
projectID := strings.TrimSpace(chi.URLParam(r, "projectId"))
|
||||||
|
if projectID == "" {
|
||||||
|
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_request", "Project ID is required.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := decodeDeleteProjectFolderRequest(r)
|
||||||
|
if strings.TrimSpace(payload.FolderID) == "" {
|
||||||
|
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_request", "Folder ID is required.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := routes.bootstrapService().DeleteProjectTreeFolder(r.Context(), bootstrapservice.DeleteProjectFolderInput{
|
||||||
|
ProjectID: projectID,
|
||||||
|
FolderID: payload.FolderID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
routes.writeProjectFolderError(w, r, err, "delete")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteJSON(w, http.StatusOK, map[string]any{
|
||||||
|
"data": result,
|
||||||
|
"meta": map[string]any{
|
||||||
|
"resource": "project-tree-folder-delete",
|
||||||
|
"persisted": true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (routes apiRoutes) writeProjectFolderError(w http.ResponseWriter, r *http.Request, err error, operation string) {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, bootstrapservice.ErrProjectNotFound), errors.Is(err, bootstrapservice.ErrProjectFolderNotFound):
|
case errors.Is(err, bootstrapservice.ErrProjectNotFound), errors.Is(err, bootstrapservice.ErrProjectFolderNotFound):
|
||||||
WriteError(w, http.StatusNotFound, RequestIDFromContext(r.Context()), "not_found", err.Error())
|
WriteError(w, http.StatusNotFound, RequestIDFromContext(r.Context()), "not_found", err.Error())
|
||||||
default:
|
default:
|
||||||
routes.cfg.Logger.Error("persist project folder", "error", err, "path", r.URL.Path)
|
routes.cfg.Logger.Error(operation+" project folder", "error", err, "path", r.URL.Path)
|
||||||
message := "Failed to persist project folder."
|
message := "Failed to " + operation + " project folder."
|
||||||
if routes.cfg.Config.IsDevelopment() {
|
if routes.cfg.Config.IsDevelopment() {
|
||||||
message = message + " " + err.Error()
|
message = message + " " + err.Error()
|
||||||
}
|
}
|
||||||
WriteError(w, http.StatusInternalServerError, RequestIDFromContext(r.Context()), "project_folder_persist_failed", message)
|
WriteError(w, http.StatusInternalServerError, RequestIDFromContext(r.Context()), "project_folder_"+operation+"_failed", message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeDeleteProjectFolderRequest(r *http.Request) deleteProjectFolderRequest {
|
||||||
|
return deleteProjectFolderRequest{
|
||||||
|
FolderID: strings.TrimSpace(r.URL.Query().Get("folderId")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,8 +36,10 @@ func (routes apiRoutes) Register(router chi.Router) {
|
|||||||
apiRouter.Route("/projects/{projectId}", func(projectRouter chi.Router) {
|
apiRouter.Route("/projects/{projectId}", func(projectRouter chi.Router) {
|
||||||
projectRouter.Get("/folders", routes.handleProjectFolders)
|
projectRouter.Get("/folders", routes.handleProjectFolders)
|
||||||
projectRouter.Post("/folders", routes.handleCreateProjectFolder)
|
projectRouter.Post("/folders", routes.handleCreateProjectFolder)
|
||||||
|
projectRouter.Delete("/folders", routes.handleDeleteProjectFolder)
|
||||||
projectRouter.Get("/tree/folders", routes.handleProjectTreeFolders)
|
projectRouter.Get("/tree/folders", routes.handleProjectTreeFolders)
|
||||||
projectRouter.Post("/tree/folders", routes.handleCreateProjectTreeFolder)
|
projectRouter.Post("/tree/folders", routes.handleCreateProjectTreeFolder)
|
||||||
|
projectRouter.Delete("/tree/folders", routes.handleDeleteProjectTreeFolder)
|
||||||
})
|
})
|
||||||
|
|
||||||
if routes.cfg.Config.IsDevelopment() {
|
if routes.cfg.Config.IsDevelopment() {
|
||||||
|
|||||||
@@ -658,28 +658,65 @@ export const ProjectSelector = (props: ProjectSelectorProps): JSX.Element => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deletePersistedFolder = async (folderId: string): Promise<void> => {
|
||||||
|
const projectId = selectedProject().id;
|
||||||
|
if (!folderId || !isUuidString(projectId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${resolveAPIBase()}/projects/${projectId}/folders?folderId=${encodeURIComponent(folderId)}`,
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const body = (await response.json()) as ProjectFoldersResponse;
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(body.message || "Failed to delete project folder.");
|
||||||
|
}
|
||||||
|
|
||||||
|
setPersistedFolders(readPersistedFolders(body));
|
||||||
|
setCollapsedFolderIds((current) => current.filter((id) => id !== folderId));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const cancelPendingFolder = (): void => {
|
const cancelPendingFolder = (): void => {
|
||||||
setPendingFolderDraft(null);
|
setPendingFolderDraft(null);
|
||||||
setPendingFolderName("");
|
setPendingFolderName("");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleContextActionSelect = (action: { id: string; label: string }, target: ProjectMenuTarget): void => {
|
const handleContextActionSelect = (action: { id: string; label: string }, target: ProjectMenuTarget): void => {
|
||||||
if (action.id !== "new-folder") {
|
switch (action.id) {
|
||||||
return;
|
case "new-folder":
|
||||||
}
|
switch (target.kind) {
|
||||||
|
case "surface":
|
||||||
switch (target.kind) {
|
beginFolderDraft(null, 0);
|
||||||
case "surface":
|
return;
|
||||||
beginFolderDraft(null, 0);
|
case "folder":
|
||||||
|
beginFolderDraft(target.id, (findTreeNodeDepth(projectTreeNodes(), target.id, projectTreeAdapter) ?? 0) + 1);
|
||||||
|
return;
|
||||||
|
case "project": {
|
||||||
|
const parentId = findTreeNodeLocation(projectTreeNodes(), target.id, projectTreeAdapter)?.parentId ?? null;
|
||||||
|
beginFolderDraft(parentId, parentId ? (findTreeNodeDepth(projectTreeNodes(), parentId, projectTreeAdapter) ?? 0) + 1 : 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
case "folder":
|
case "delete-folder":
|
||||||
beginFolderDraft(target.id, (findTreeNodeDepth(projectTreeNodes(), target.id, projectTreeAdapter) ?? 0) + 1);
|
if (target.kind === "folder") {
|
||||||
|
void deletePersistedFolder(target.id);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
case "project": {
|
default:
|
||||||
const parentId = findTreeNodeLocation(projectTreeNodes(), target.id, projectTreeAdapter)?.parentId ?? null;
|
|
||||||
beginFolderDraft(parentId, parentId ? (findTreeNodeDepth(projectTreeNodes(), parentId, projectTreeAdapter) ?? 0) + 1 : 0);
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -581,6 +581,36 @@ export const WorkspaceSidebar = (props: WorkspaceSidebarProps): JSX.Element => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deletePersistedFolder = async (folderId: string): Promise<void> => {
|
||||||
|
const projectId = activeProject()?.id ?? "";
|
||||||
|
if (!folderId || !projectId || !isUuidString(projectId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${resolveAPIBase()}/projects/${projectId}/tree/folders?folderId=${encodeURIComponent(folderId)}`,
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const body = (await response.json()) as WorkspaceFoldersResponse;
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(body.message || "Failed to delete project tree folder.");
|
||||||
|
}
|
||||||
|
|
||||||
|
setPersistedFolders(readPersistedWorkspaceFolders(body));
|
||||||
|
setCollapsedFolderIds((current) => current.filter((id) => id !== folderId));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const cancelPendingFolder = (): void => {
|
const cancelPendingFolder = (): void => {
|
||||||
setPendingFolderDraft(null);
|
setPendingFolderDraft(null);
|
||||||
setPendingFolderName("");
|
setPendingFolderName("");
|
||||||
@@ -639,20 +669,27 @@ export const WorkspaceSidebar = (props: WorkspaceSidebarProps): JSX.Element => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleContextActionSelect = (action: WorkspaceContextMenuAction, target: WorkspaceContextMenuTarget): void => {
|
const handleContextActionSelect = (action: WorkspaceContextMenuAction, target: WorkspaceContextMenuTarget): void => {
|
||||||
if (action.id !== "new-folder") {
|
switch (action.id) {
|
||||||
return;
|
case "new-folder":
|
||||||
}
|
switch (target.kind) {
|
||||||
|
case "workspace":
|
||||||
switch (target.kind) {
|
case "home":
|
||||||
case "workspace":
|
beginFolderDraft(null, 0);
|
||||||
case "home":
|
return;
|
||||||
beginFolderDraft(null, 0);
|
case "folder":
|
||||||
|
beginFolderDraft(target.id, (findTreeNodeDepth(workspaceTreeNodes(), target.id, workspaceTreeAdapter) ?? 0) + 1);
|
||||||
|
return;
|
||||||
|
case "settings":
|
||||||
|
case "item":
|
||||||
|
return;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
case "folder":
|
case "delete-folder":
|
||||||
beginFolderDraft(target.id, (findTreeNodeDepth(workspaceTreeNodes(), target.id, workspaceTreeAdapter) ?? 0) + 1);
|
if (target.kind === "folder") {
|
||||||
|
void deletePersistedFolder(target.id);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
case "settings":
|
default:
|
||||||
case "item":
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -597,6 +597,11 @@ const getProjectCreateActions = (): readonly ProjectContextMenuAction[] =>
|
|||||||
{ id: "new-folder", label: "New folder" },
|
{ id: "new-folder", label: "New folder" },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
const getProjectFolderDangerActions = (): readonly ProjectContextMenuAction[] =>
|
||||||
|
[
|
||||||
|
{ id: "delete-folder", label: "Delete", shortcut: { modifiers: ["meta"], key: "delete" }, tone: "danger" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
export const createProjectSurfaceTarget = (label = "Projects"): ProjectMenuTarget => ({
|
export const createProjectSurfaceTarget = (label = "Projects"): ProjectMenuTarget => ({
|
||||||
id: "project-surface",
|
id: "project-surface",
|
||||||
label,
|
label,
|
||||||
@@ -643,6 +648,10 @@ export const getProjectContextMenuSections = (target: ProjectMenuTarget): readon
|
|||||||
id: "create",
|
id: "create",
|
||||||
items: createActions,
|
items: createActions,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "organize",
|
||||||
|
items: getProjectFolderDangerActions(),
|
||||||
|
},
|
||||||
] as const;
|
] as const;
|
||||||
case "project":
|
case "project":
|
||||||
return [
|
return [
|
||||||
|
|||||||
Reference in New Issue
Block a user