Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(web): project recycle bin #1181

Merged
merged 31 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
990a500
initial implemention of recyle bin
mkumbobeaty Oct 11, 2024
d788229
Merge branch 'main' of github.com:reearth/reearth-visualizer into fea…
mkumbobeaty Oct 16, 2024
357f863
implement recycle bin page and functionalities
mkumbobeaty Oct 16, 2024
bb1c9ad
fix translation
mkumbobeaty Oct 16, 2024
ea5cf3d
Merge branch 'main' of github.com:reearth/reearth-visualizer into fea…
mkumbobeaty Oct 16, 2024
7db6db7
exclude deleted project
hexaforce Oct 17, 2024
d7bd14c
modify the 'UpdateProject' response
hexaforce Oct 17, 2024
99c912c
modify the 'DeletedProjects' response
hexaforce Oct 17, 2024
5b018e6
update the delete logic
mkumbobeaty Oct 17, 2024
9454c95
Merge branch 'main' of github.com:reearth/reearth-visualizer into fea…
mkumbobeaty Oct 17, 2024
2fd25f2
rename component
mkumbobeaty Oct 18, 2024
1dfdf5e
fix typo
mkumbobeaty Oct 18, 2024
7462265
Merge branch 'main' of github.com:reearth/reearth-visualizer into fea…
mkumbobeaty Oct 18, 2024
75efa5a
fix typo
mkumbobeaty Oct 18, 2024
22eb8ff
improve notifications
mkumbobeaty Oct 18, 2024
c1f90b4
remove console
mkumbobeaty Oct 18, 2024
d5c1e84
improve recycle bin implemention
mkumbobeaty Oct 18, 2024
7142284
improvement and fix typo
mkumbobeaty Oct 18, 2024
29ab921
update translation
mkumbobeaty Oct 18, 2024
d537b61
fix
mkumbobeaty Oct 18, 2024
7e4639a
refactor delete logicd
mkumbobeaty Oct 18, 2024
25059bf
refactor delete logics
mkumbobeaty Oct 18, 2024
6a942f2
Merge branch 'feat/project-recylebin' of github.com:reearth/reearth-v…
mkumbobeaty Oct 18, 2024
d448594
Merge branch 'main' of github.com:reearth/reearth-visualizer into fea…
mkumbobeaty Oct 21, 2024
b4d23e9
fix translation conflict
mkumbobeaty Oct 21, 2024
b7db495
improve publish when project is removed
mkumbobeaty Oct 21, 2024
9b209d0
fix based on comments
mkumbobeaty Oct 21, 2024
f9b2362
update the naming
mkumbobeaty Oct 22, 2024
46edbd8
add word consistance
mkumbobeaty Oct 23, 2024
9c4ee8e
Merge branch 'main' of github.com:reearth/reearth-visualizer into fea…
mkumbobeaty Oct 23, 2024
ebcef69
Merge branch 'main' of github.com:reearth/reearth-visualizer into fea…
mkumbobeaty Oct 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 50 additions & 32 deletions server/e2e/gql_project_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,10 +210,15 @@ func TestFindStarredByWorkspace(t *testing.T) {
project2ID := createProject(e, "Project 2")
project3ID := createProject(e, "Project 3")
project4ID := createProject(e, "Project 4")
project5ID := createProject(e, "Project 5")

starProject(e, project1ID)
starProject(e, project3ID)

// star and deleted 'Project 5'
starProject(e, project5ID)
deleteProject(e, project5ID)

requestBody := GraphQLRequest{
OperationName: "GetStarredProjects",
Query: `
Expand Down Expand Up @@ -251,6 +256,8 @@ func TestFindStarredByWorkspace(t *testing.T) {
nodeCount := int(nodes.Length().Raw())
assert.Equal(t, 2, nodeCount, "Expected 2 nodes in the response")

nodes.Length().Equal(2) // 'Project 1' and 'Project 3'

starredProjectsMap := make(map[string]bool)
for _, node := range nodes.Iter() {
obj := node.Object()
Expand All @@ -275,7 +282,6 @@ func TestFindStarredByWorkspace(t *testing.T) {
assert.True(t, starredProjectsMap[project3ID], "Project 3 should be starred")
assert.False(t, starredProjectsMap[project2ID], "Project 2 should not be starred")
assert.False(t, starredProjectsMap[project4ID], "Project 4 should not be starred")

}

func starProject(e *httpexpect.Expect, projectID string) {
Expand Down Expand Up @@ -467,39 +473,10 @@ func TestDeleteProjects(t *testing.T) {
createProject(e, "project3-test")

// Deleted 'project2'
requestBody := GraphQLRequest{
OperationName: "UpdateProject",
Query: `mutation UpdateProject($input: UpdateProjectInput!) {
updateProject(input: $input) {
project {
id
name
isDeleted
updatedAt
__typename
}
__typename
}
}`,
Variables: map[string]any{
"input": map[string]any{
"projectId": project2ID,
"deleted": true,
},
},
}

e.POST("/api/graphql").
WithHeader("Origin", "https://example.com").
WithHeader("X-Reearth-Debug-User", uID.String()).
WithHeader("Content-Type", "application/json").
WithJSON(requestBody).
Expect().
Status(http.StatusOK).
JSON()
deleteProject(e, project2ID)

// check
requestBody = GraphQLRequest{
requestBody := GraphQLRequest{
OperationName: "GetDeletedProjects",
Query: `
query GetDeletedProjects($teamId: ID!) {
Expand Down Expand Up @@ -530,3 +507,44 @@ func TestDeleteProjects(t *testing.T) {
deletedProjects.Value("nodes").Array().Length().Equal(1)
deletedProjects.Value("nodes").Array().First().Object().Value("name").Equal("project2-test")
}

func deleteProject(e *httpexpect.Expect, projectID string) {

updateProjectMutation := GraphQLRequest{
OperationName: "UpdateProject",
Query: `mutation UpdateProject($input: UpdateProjectInput!) {
updateProject(input: $input) {
project {
id
name
isDeleted
updatedAt
__typename
}
__typename
}
}`,
Variables: map[string]any{
"input": map[string]any{
"projectId": projectID,
"deleted": true,
},
},
}

response := e.POST("/api/graphql").
WithHeader("Origin", "https://example.com").
WithHeader("X-Reearth-Debug-User", uID.String()).
WithHeader("Content-Type", "application/json").
WithJSON(updateProjectMutation).
Expect().
Status(http.StatusOK).
JSON().
Object().
Value("data").Object().
Value("updateProject").Object().
Value("project").Object()

response.ValueEqual("id", projectID).
ValueEqual("isDeleted", true)
}
Comment on lines +510 to +550
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

🛠️ Refactor suggestion

'deleteProject' Mutation Exists and Can Be Utilized

  • The GraphQL schema includes a deleteProject mutation, which can directly mark a project as deleted.
  • It's advisable to rename the deleteProject function and update its implementation to use the deleteProject mutation instead of updateProject.
🔗 Analysis chain

Recommendation: Rename 'deleteProject' function for clarity

The deleteProject function uses the updateProject mutation to mark a project as deleted. Renaming the function to reflect its operation more accurately can improve code readability.

Apply this diff to rename the function:

-func deleteProject(e *httpexpect.Expect, projectID string) {
+func markProjectAsDeleted(e *httpexpect.Expect, projectID string) {

Additionally, if the API supports a dedicated deleteProject mutation, consider using it for a more precise operation.

To check if a deleteProject mutation exists, run the following script:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify if a 'deleteProject' mutation exists in the GraphQL schema.

# Expected Result: If 'deleteProject' exists, it will be listed in the output.

# Command:
gh api graphql -F query='{ __schema { mutationType { fields { name } } } }' | grep deleteProject

Length of output: 8684

1 change: 1 addition & 0 deletions server/internal/adapter/gql/gqlmodel/convert_project.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ func ToProject(p *project.Project) *Project {
EnableGa: p.EnableGA(),
TrackingID: p.TrackingID(),
Starred: p.Starred(),
IsDeleted: p.IsDeleted(),
}
}

Expand Down
1 change: 1 addition & 0 deletions server/internal/infrastructure/mongo/mongodoc/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,5 +128,6 @@ func (d *ProjectDocument) Model() (*project.Project, error) {
TrackingID(d.TrackingID).
// Scene(scene).
Starred(d.Starred).
Deleted(d.Deleted).
Build()
}
4 changes: 4 additions & 0 deletions server/internal/infrastructure/mongo/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ func (r *Project) FindStarredByWorkspace(ctx context.Context, id accountdomain.W
filter := bson.M{
"team": id.String(),
"starred": true,
"$or": []bson.M{
{"deleted": false},
{"deleted": bson.M{"$exists": false}},
},
}

return r.find(ctx, filter)
Expand Down
5 changes: 5 additions & 0 deletions server/pkg/project/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,8 @@ func (b *Builder) Starred(starred bool) *Builder {
b.p.starred = starred
return b
}

func (b *Builder) Deleted(deleted bool) *Builder {
b.p.isDeleted = deleted
return b
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Button, PopupMenu, TextInput } from "@reearth/beta/lib/reearth-ui";
import { styled, useTheme } from "@reearth/services/theme";
import { FC } from "react";

import ProjectDeleteModal from "../ProjectDeleteModal";

import useHooks from "./hooks";
import { ProjectProps } from "./types";

Expand All @@ -10,7 +12,8 @@ const ProjectGridViewItem: FC<ProjectProps> = ({
selectedProjectId,
onProjectOpen,
onProjectSelect,
onProjectUpdate
onProjectUpdate,
onProjectDelete
}) => {
const theme = useTheme();

Expand All @@ -21,19 +24,23 @@ const ProjectGridViewItem: FC<ProjectProps> = ({
isHovered,
isStarred,
hasMapOrStoryPublished,
projectDeleteModalVisible,
handleProjectNameChange,
handleProjectNameBlur,
handleProjectHover,
handleProjectNameDoubleClick,
handleProjectStarClick
handleProjectStarClick,
handleDeleteModelClose,
handleProjectDelete
// exportModalVisible,
// closeExportModal,
// handleExportProject
} = useHooks({
project,
selectedProjectId,
onProjectUpdate,
onProjectSelect
onProjectSelect,
onProjectDelete
});

return (
Expand Down Expand Up @@ -87,6 +94,13 @@ const ProjectGridViewItem: FC<ProjectProps> = ({
/>
</CardFooter>
</Card>
{projectDeleteModalVisible && (
<ProjectDeleteModal
isVisible={projectDeleteModalVisible}
onClose={handleDeleteModelClose}
onProjectDelete={() => handleProjectDelete(project.id)}
/>
)}
{/* MEMO: this modal will be used in the future */}
{/* <Modal visible={exportModalVisible} size="small">
<ModalPanel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ type Props = {
selectedProjectId?: string;
onProjectUpdate?: (project: ProjectType, projectId: string) => void;
onProjectSelect?: (e?: MouseEvent<Element>, projectId?: string) => void;
onProjectDelete?: (projectId?: string) => void;
};

export default ({
project,
selectedProjectId,
onProjectUpdate,
onProjectSelect
onProjectSelect,
onProjectDelete
}: Props) => {
const t = useT();
const { useStoriesQuery } = useStorytellingFetcher();
Expand All @@ -32,6 +34,8 @@ export default ({
const [projectName, setProjectName] = useState(project.name);
const [isHovered, setIsHovered] = useState(false);
const [isStarred, setIsStarred] = useState(project.starred);
const [projectDeleteModalVisible, setProjectDeleteModalVisible] = useState(false);

// MEMO: this modal state and function will be used in the future
// const [exportModalVisible, setExportModalVisible] = useState(false);

Expand Down Expand Up @@ -79,6 +83,14 @@ export default ({
setIsStarred(project.starred);
}, [project.starred]);

const handleDeleteModelOpen = useCallback(() => {
setProjectDeleteModalVisible(true);
}, []);

const handleDeleteModelClose = useCallback(() => {
setProjectDeleteModalVisible(false);
}, []);

const popupMenu: PopupMenuItem[] = [
{
id: "rename",
Expand All @@ -97,6 +109,12 @@ export default ({
title: t("Export"),
icon: "downloadSimple",
onClick: () => handleExportProject()
},
{
id: "remove",
title: t("Remove"),
icon: "trash",
onClick: () => handleDeleteModelOpen()
}
];

Expand Down Expand Up @@ -143,18 +161,28 @@ export default ({
return hasMapPublished || hasStoryPublished;
}, [stories, project.status]);

const handleProjectDelete = useCallback((projectId?: string) => {
if (!projectId) return;
onProjectDelete?.(projectId);
handleDeleteModelClose()
}, [handleDeleteModelClose, onProjectDelete])

return {
isEditing,
projectName,
isHovered,
popupMenu,
isStarred,
hasMapOrStoryPublished,
projectDeleteModalVisible,
handleProjectNameChange,
handleProjectNameBlur,
handleProjectHover,
handleProjectNameDoubleClick,
handleProjectStarClick,
handleExportProject
handleExportProject,
handleDeleteModelOpen,
handleDeleteModelClose,
handleProjectDelete
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export type ProjectProps = {
onProjectOpen?: () => void;
onProjectSelect?: (e?: MouseEvent<Element>, projectId?: string) => void;
onProjectUpdate?: (project: ProjectType, projectId: string) => void;
onProjectDelete?: (projectId?: string) => void;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import {
Button,
Icon,
Modal,
ModalPanel,
Typography
} from "@reearth/beta/lib/reearth-ui";
import { useT } from "@reearth/services/i18n";
import { styled } from "@reearth/services/theme";
import { FC } from "react";

type Props = {
isVisible: boolean;
onClose: () => void;
onProjectDelete: () => void;
};
const ProjectDeleteModal: FC<Props> = ({
isVisible,
onClose,
onProjectDelete
}) => {
const t = useT();
return (
<Modal size="small" visible={isVisible}>
<ModalPanel
actions={
<>
<Button size="normal" title="Cancel" onClick={onClose} />
<Button
size="normal"
title="Remove"
appearance="dangerous"
onClick={onProjectDelete}
/>
</>
}
appearance="simple"
>
<Wrapper>
<WarningIcon icon="warning" />
<Typography size="body">
{t("Your project will be move to trash.")}
</Typography>
<Typography size="body">
{t(
"This means the project will no longer be published. But you can still see and restore you project from recycle bin."
)}
</Typography>
</Wrapper>
</ModalPanel>
</Modal>
);
};

export default ProjectDeleteModal;

const Wrapper = styled("div")(({ theme }) => ({
display: "flex",
flexDirection: "column",
padding: theme.spacing.large,
gap: theme.spacing.normal
}));

const WarningIcon = styled(Icon)(({ theme }) => ({
width: "24px",
height: "24px",
color: theme.warning.main
}));
Loading
Loading