diff --git a/server/e2e/gql_project_test.go b/server/e2e/gql_project_test.go index aad2e57ad..a6b41c741 100644 --- a/server/e2e/gql_project_test.go +++ b/server/e2e/gql_project_test.go @@ -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", + }, + }, + } + + 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") +} diff --git a/server/go.mod b/server/go.mod index 885587c4a..a785d42bd 100644 --- a/server/go.mod +++ b/server/go.mod @@ -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 @@ -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 diff --git a/server/internal/adapter/gql/loader_project.go b/server/internal/adapter/gql/loader_project.go index 8b0c34080..850dae687 100644 --- a/server/internal/adapter/gql/loader_project.go +++ b/server/internal/adapter/gql/loader_project.go @@ -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 } diff --git a/server/internal/adapter/gql/resolver_query.go b/server/internal/adapter/gql/resolver_query.go index 2ed6d9196..3e682f5a4 100644 --- a/server/internal/adapter/gql/resolver_query.go +++ b/server/internal/adapter/gql/resolver_query.go @@ -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) { diff --git a/server/internal/adapter/gql/resolver_team.go b/server/internal/adapter/gql/resolver_team.go index 3f5470e1b..65ab4aec6 100644 --- a/server/internal/adapter/gql/resolver_team.go +++ b/server/internal/adapter/gql/resolver_team.go @@ -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, diff --git a/server/internal/infrastructure/memory/project.go b/server/internal/infrastructure/memory/project.go index 13688864f..5c62603e3 100644 --- a/server/internal/infrastructure/memory/project.go +++ b/server/internal/infrastructure/memory/project.go @@ -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) } } diff --git a/server/internal/infrastructure/mongo/project.go b/server/internal/infrastructure/mongo/project.go index cda562ed0..74fea087b 100644 --- a/server/internal/infrastructure/mongo/project.go +++ b/server/internal/infrastructure/mongo/project.go @@ -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) } diff --git a/server/internal/usecase/interactor/project.go b/server/internal/usecase/interactor/project.go index 23743bcc0..86507a7af 100644 --- a/server/internal/usecase/interactor/project.go +++ b/server/internal/usecase/interactor/project.go @@ -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, }) } diff --git a/server/internal/usecase/interfaces/project.go b/server/internal/usecase/interfaces/project.go index 069e13284..eeb6bf90b 100644 --- a/server/internal/usecase/interfaces/project.go +++ b/server/internal/usecase/interfaces/project.go @@ -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) 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) diff --git a/server/internal/usecase/repo/project.go b/server/internal/usecase/repo/project.go index 7ba88d180..d867783ea 100644 --- a/server/internal/usecase/repo/project.go +++ b/server/internal/usecase/repo/project.go @@ -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 { diff --git a/web/src/beta/features/Dashboard/ContentsContainer/Projects/Project/ProjectGridViewItem.tsx b/web/src/beta/features/Dashboard/ContentsContainer/Projects/Project/ProjectGridViewItem.tsx index 301788636..732b8deb4 100644 --- a/web/src/beta/features/Dashboard/ContentsContainer/Projects/Project/ProjectGridViewItem.tsx +++ b/web/src/beta/features/Dashboard/ContentsContainer/Projects/Project/ProjectGridViewItem.tsx @@ -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"; @@ -10,6 +18,7 @@ const ProjectGridViewItem: FC = ({ selectedProjectId, onProjectOpen, onProjectSelect, + onArchiveProject, onProjectUpdate }) => { const theme = useTheme(); @@ -21,10 +30,13 @@ const ProjectGridViewItem: FC = ({ isHovered, isStarred, hasMapOrStoryPublished, + archiveOpen, handleProjectNameChange, handleProjectNameBlur, handleProjectHover, handleProjectNameDoubleClick, + handleArchivedModal, + handleProjectArchived, handleProjectStarClick // exportModalVisible, // closeExportModal, @@ -33,9 +45,12 @@ const ProjectGridViewItem: FC = ({ project, selectedProjectId, onProjectUpdate, + onArchiveProject, onProjectSelect }); + const t = useT(); + return ( <> @@ -98,10 +113,51 @@ const ProjectGridViewItem: FC = ({ */} + + handleArchivedModal(false)} + actions={ + <> +