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,server): Projects Recycle bin #1160

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
133 changes: 133 additions & 0 deletions server/e2e/gql_project_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -452,3 +452,136 @@ func TestSortByUpdatedAt(t *testing.T) {
edges.Element(1).Object().Value("node").Object().Value("name").Equal("project3-test")
edges.Element(2).Object().Value("node").Object().Value("name").Equal("project1-test")
}

// go test -v -run TestArchiveProject ./e2e/...

func TestArchiveProject(t *testing.T) {

e := StartServer(t, &config.Config{
Origins: []string{"https://example.com"},
AuthSrv: config.AuthSrvConfig{
Disabled: true,
},
}, true, baseSeeder)

createProject(e, "project1-test")
project2ID := createProject(e, "project2-test")
createProject(e, "project3-test")

// Archive 'project2'
requestBody := GraphQLRequest{
OperationName: "ArchiveProject",
Query: `mutation ArchiveProject($projectId: ID!, $archived: Boolean!) {
updateProject(input: {projectId: $projectId, archived: $archived}) {
project {
id
isArchived
__typename
}
__typename
}
}`,
Variables: map[string]any{
"projectId": project2ID,
"archived": 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()

requestBody = GraphQLRequest{
OperationName: "GetProjects",
Query: `query GetProjects($teamId: ID!, $includeArchived: Boolean, $pagination: Pagination, $keyword: String, $sort: ProjectSort) {
projects(teamId: $teamId, includeArchived: $includeArchived, pagination: $pagination, keyword: $keyword, sort: $sort) {
edges {
node {
id
...ProjectFragment
scene {
id
__typename
}
__typename
}
__typename
}
nodes {
id
...ProjectFragment
scene {
id
__typename
}
__typename
}
pageInfo {
endCursor
hasNextPage
hasPreviousPage
startCursor
__typename
}
totalCount
__typename
}
}
fragment ProjectFragment on Project {
id
name
description
imageUrl
isArchived
isBasicAuthActive
basicAuthUsername
basicAuthPassword
publicTitle
publicDescription
publicImage
alias
enableGa
trackingId
publishmentStatus
updatedAt
createdAt
coreSupport
starred
__typename
}`,
Variables: map[string]any{
"teamId": wID.String(),
"includeArchived": false,
"pagination": map[string]any{
"first": 16,
},
"sort": map[string]string{
"field": "UPDATEDAT",
"direction": "DESC",
},
},
}
hexaforce marked this conversation as resolved.
Show resolved Hide resolved

edges := 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().
Object().
Value("data").Object().
Value("projects").Object().
Value("edges").Array()

// `project2-test` is archived and will not be retrieved
edges.Length().Equal(3)
edges.Element(0).Object().Value("node").Object().Value("name").Equal("project3-test")
edges.Element(1).Object().Value("node").Object().Value("name").Equal("project1-test")
edges.Element(2).Object().Value("node").Object().Value("name").Equal("p1")
}
2 changes: 1 addition & 1 deletion server/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ require (
github.com/kennygrant/sanitize v1.2.4
github.com/labstack/echo/v4 v4.11.4
github.com/mitchellh/mapstructure v1.5.0
github.com/oklog/ulid v1.3.1
github.com/paulmach/go.geojson v1.4.0
github.com/pkg/errors v0.9.1
github.com/ravilushqa/otelgqlgen v0.15.0
Expand Down Expand Up @@ -116,7 +117,6 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
github.com/nicksnyder/go-i18n/v2 v2.4.0 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rs/cors v1.10.1 // indirect
Expand Down
4 changes: 2 additions & 2 deletions server/internal/adapter/gql/loader_project.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,13 @@ func (c *ProjectLoader) Fetch(ctx context.Context, ids []gqlmodel.ID) ([]*gqlmod
return projects, nil
}

func (c *ProjectLoader) FindByWorkspace(ctx context.Context, wsID gqlmodel.ID, keyword *string, sort *project.SortType, pagination *gqlmodel.Pagination) (*gqlmodel.ProjectConnection, error) {
func (c *ProjectLoader) FindByWorkspace(ctx context.Context, wsID gqlmodel.ID, includeArchived *bool, keyword *string, sort *project.SortType, pagination *gqlmodel.Pagination) (*gqlmodel.ProjectConnection, error) {
tid, err := gqlmodel.ToID[accountdomain.Workspace](wsID)
if err != nil {
return nil, err
}

res, pi, err := c.usecase.FindByWorkspace(ctx, tid, keyword, sort, gqlmodel.ToPagination(pagination), getOperator(ctx))
res, pi, err := c.usecase.FindByWorkspace(ctx, tid, includeArchived, keyword, sort, gqlmodel.ToPagination(pagination), getOperator(ctx))
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion server/internal/adapter/gql/resolver_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ func (r *queryResolver) Scene(ctx context.Context, projectID gqlmodel.ID) (*gqlm
}

func (r *queryResolver) Projects(ctx context.Context, teamID gqlmodel.ID, includeArchived *bool, pagination *gqlmodel.Pagination, keyword *string, sortType *gqlmodel.ProjectSort) (*gqlmodel.ProjectConnection, error) {
return loaders(ctx).Project.FindByWorkspace(ctx, teamID, keyword, gqlmodel.ProjectSortTypeFrom(sortType), pagination)
return loaders(ctx).Project.FindByWorkspace(ctx, teamID, includeArchived, keyword, gqlmodel.ProjectSortTypeFrom(sortType), pagination)
}

func (r *queryResolver) DatasetSchemas(ctx context.Context, sceneID gqlmodel.ID, first *int, last *int, after *usecasex.Cursor, before *usecasex.Cursor) (*gqlmodel.DatasetSchemaConnection, error) {
Expand Down
2 changes: 1 addition & 1 deletion server/internal/adapter/gql/resolver_team.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func (r *teamResolver) Assets(ctx context.Context, obj *gqlmodel.Team, first *in
}

func (r *teamResolver) Projects(ctx context.Context, obj *gqlmodel.Team, includeArchived *bool, first *int, last *int, after *usecasex.Cursor, before *usecasex.Cursor) (*gqlmodel.ProjectConnection, error) {
return loaders(ctx).Project.FindByWorkspace(ctx, obj.ID, nil, nil, &gqlmodel.Pagination{
return loaders(ctx).Project.FindByWorkspace(ctx, obj.ID, includeArchived, nil, nil, &gqlmodel.Pagination{
First: first,
Last: last,
After: after,
Expand Down
3 changes: 3 additions & 0 deletions server/internal/infrastructure/memory/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ func (r *Project) FindByWorkspace(ctx context.Context, id accountdomain.Workspac
result := []*project.Project{}
for _, d := range r.data {
if d.Workspace() == id && (filter.Keyword == nil || strings.Contains(d.Name(), *filter.Keyword)) {
if filter.IncludeArchived != nil && !*filter.IncludeArchived && d.IsArchived() {
continue
}
result = append(result, d)
}
}
Expand Down
6 changes: 6 additions & 0 deletions server/internal/infrastructure/mongo/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ func (r *Project) FindByWorkspace(ctx context.Context, id accountdomain.Workspac
})
}

if uFilter.IncludeArchived != nil {
if !*uFilter.IncludeArchived {
filter = mongox.And(filter, "archived", false)
}
}

return r.paginate(ctx, filter, uFilter.Sort, uFilter.Pagination)
}

Expand Down
9 changes: 5 additions & 4 deletions server/internal/usecase/interactor/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,12 @@ func (i *Project) Fetch(ctx context.Context, ids []id.ProjectID, _ *usecase.Oper
return i.projectRepo.FindByIDs(ctx, ids)
}

func (i *Project) FindByWorkspace(ctx context.Context, id accountdomain.WorkspaceID, keyword *string, sort *project.SortType, p *usecasex.Pagination, operator *usecase.Operator) ([]*project.Project, *usecasex.PageInfo, error) {
func (i *Project) FindByWorkspace(ctx context.Context, id accountdomain.WorkspaceID, includeArchived *bool, keyword *string, sort *project.SortType, p *usecasex.Pagination, operator *usecase.Operator) ([]*project.Project, *usecasex.PageInfo, error) {
return i.projectRepo.FindByWorkspace(ctx, id, repo.ProjectFilter{
Pagination: p,
Sort: sort,
Keyword: keyword,
IncludeArchived: includeArchived,
Pagination: p,
Sort: sort,
Keyword: keyword,
})
}

Expand Down
2 changes: 1 addition & 1 deletion server/internal/usecase/interfaces/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ var (

type Project interface {
Fetch(context.Context, []id.ProjectID, *usecase.Operator) ([]*project.Project, error)
FindByWorkspace(context.Context, accountdomain.WorkspaceID, *string, *project.SortType, *usecasex.Pagination, *usecase.Operator) ([]*project.Project, *usecasex.PageInfo, error)
FindByWorkspace(context.Context, accountdomain.WorkspaceID, *bool, *string, *project.SortType, *usecasex.Pagination, *usecase.Operator) ([]*project.Project, *usecasex.PageInfo, error)
hexaforce marked this conversation as resolved.
Show resolved Hide resolved
FindStarredByWorkspace(context.Context, accountdomain.WorkspaceID, *usecase.Operator) ([]*project.Project, error)
Create(context.Context, CreateProjectParam, *usecase.Operator) (*project.Project, error)
Update(context.Context, UpdateProjectParam, *usecase.Operator) (*project.Project, error)
Expand Down
7 changes: 4 additions & 3 deletions server/internal/usecase/repo/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import (
)

type ProjectFilter struct {
Sort *project.SortType
Keyword *string
Pagination *usecasex.Pagination
IncludeArchived *bool
Sort *project.SortType
Keyword *string
Pagination *usecasex.Pagination
}

type Project interface {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { Button, PopupMenu, TextInput } from "@reearth/beta/lib/reearth-ui";
import {
Button,
PopupMenu,
Typography,
TextInput,
Modal,
ModalPanel
} from "@reearth/beta/lib/reearth-ui";
import { useT } from "@reearth/services/i18n";
import { styled, useTheme } from "@reearth/services/theme";
import { FC } from "react";

Expand All @@ -10,6 +18,7 @@ const ProjectGridViewItem: FC<ProjectProps> = ({
selectedProjectId,
onProjectOpen,
onProjectSelect,
onArchiveProject,
onProjectUpdate
}) => {
const theme = useTheme();
Expand All @@ -21,10 +30,13 @@ const ProjectGridViewItem: FC<ProjectProps> = ({
isHovered,
isStarred,
hasMapOrStoryPublished,
archiveOpen,
handleProjectNameChange,
handleProjectNameBlur,
handleProjectHover,
handleProjectNameDoubleClick,
handleArchivedModal,
handleProjectArchived,
handleProjectStarClick
// exportModalVisible,
// closeExportModal,
Expand All @@ -33,9 +45,12 @@ const ProjectGridViewItem: FC<ProjectProps> = ({
project,
selectedProjectId,
onProjectUpdate,
onArchiveProject,
onProjectSelect
});

const t = useT();

return (
<>
<Card>
Expand Down Expand Up @@ -98,10 +113,51 @@ const ProjectGridViewItem: FC<ProjectProps> = ({
<ModalContent />
</ModalPanel>
</Modal> */}
<Modal size="small" visible={archiveOpen}>
<ModalPanel
title={t("Remove")}
onCancel={() => handleArchivedModal(false)}
actions={
<>
<Button
key="cancel"
title={t("Cancel")}
appearance="secondary"
onClick={() => handleArchivedModal(false)}
size="normal"
/>
<Button
size="normal"
key="delete"
appearance="dangerous"
title={t("Delete")}
onClick={() => handleProjectArchived(true)}
disabled={!projectName}
/>
</>
}
>
<ModalContentWrapper>
<Typography size="body">
{t(
"This process will remove this project to Recycle bin. If the project was published, this also means websites and domains referencing the project will not be able to access it anymore."
)}
</Typography>
</ModalContentWrapper>
Comment on lines +141 to +145
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Correct grammatical errors in the modal message

The modal message contains grammatical errors. Please revise it for clarity:

  • Change "remove this project to Recycle bin" to "move this project to the Recycle Bin."
  • Adjust the second sentence for better readability.

Apply this diff to fix the message:

             <Typography size="body">
               {t(
-                "This process will remove this project to Recycle bin. If the project was published, this also means websites and domains referencing the project will not be able to access it anymore."
+                "This process will move this project to the Recycle Bin. If the project was published, websites and domains referencing the project will no longer have access to it."
               )}
             </Typography>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Typography size="body">
{t(
"This process will remove this project to Recycle bin. If the project was published, this also means websites and domains referencing the project will not be able to access it anymore."
)}
</Typography>
<Typography size="body">
{t(
"This process will move this project to the Recycle Bin. If the project was published, websites and domains referencing the project will no longer have access to it."
)}
</Typography>

</ModalPanel>
</Modal>
</>
Comment on lines +116 to +148
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Align terminology for archiving projects

The modal title is "Remove", and the action button is labeled "Delete", but the action moves the project to the Recycle Bin (archives it). For clarity and consistency, consider using "Archive" or "Move to Recycle Bin" for both the modal title and the action button to accurately reflect the operation.

Apply this diff to update the terminology:

       <Modal size="small" visible={archiveOpen}>
         <ModalPanel
-          title={t("Remove")}
+          title={t("Archive Project")}
           onCancel={() => handleArchivedModal(false)}
           actions={
             <>
               <Button
                 appearance="secondary"
                 onClick={() => handleArchivedModal(false)}
                 size="normal"
               />
               <Button
                 size="normal"
                 appearance="dangerous"
-                title={t("Delete")}
+                title={t("Archive")}
                 onClick={() => handleProjectArchived(true)}
                 disabled={!projectName}
               />
             </>
           }
         >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Modal size="small" visible={archiveOpen}>
<ModalPanel
title={t("Remove")}
onCancel={() => handleArchivedModal(false)}
actions={
<>
<Button
key="cancel"
title={t("Cancel")}
appearance="secondary"
onClick={() => handleArchivedModal(false)}
size="normal"
/>
<Button
size="normal"
key="delete"
appearance="dangerous"
title={t("Delete")}
onClick={() => handleProjectArchived(true)}
disabled={!projectName}
/>
</>
}
>
<ModalContentWrapper>
<Typography size="body">
{t(
"This process will remove this project to Recycle bin. If the project was published, this also means websites and domains referencing the project will not be able to access it anymore."
)}
</Typography>
</ModalContentWrapper>
</ModalPanel>
</Modal>
<Modal size="small" visible={archiveOpen}>
<ModalPanel
title={t("Archive Project")}
onCancel={() => handleArchivedModal(false)}
actions={
<>
<Button
key="cancel"
title={t("Cancel")}
appearance="secondary"
onClick={() => handleArchivedModal(false)}
size="normal"
/>
<Button
size="normal"
key="delete"
appearance="dangerous"
title={t("Archive")}
onClick={() => handleProjectArchived(true)}
disabled={!projectName}
/>
</>
}
>
<ModalContentWrapper>
<Typography size="body">
{t(
"This process will remove this project to Recycle bin. If the project was published, this also means websites and domains referencing the project will not be able to access it anymore."
)}
</Typography>
</ModalContentWrapper>
</ModalPanel>
</Modal>

);
};

const ModalContentWrapper = styled("div")(({ theme }) => ({
display: "flex",
flexDirection: "column",
gap: theme.spacing.large,
padding: theme.spacing.large,
background: theme.bg[1]
}));

export default ProjectGridViewItem;

const Card = styled("div")(() => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const ProjectListViewItem: FC<ProjectProps> = ({
selectedProjectId,
onProjectOpen,
onProjectSelect,
onArchiveProject,
onProjectUpdate
}) => {
const theme = useTheme();
Expand Down Expand Up @@ -50,6 +51,7 @@ const ProjectListViewItem: FC<ProjectProps> = ({
project,
selectedProjectId,
onProjectUpdate,
onArchiveProject,
onProjectSelect
});

Expand Down
Loading
Loading