diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 04b83e6d57..102f54ac16 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -54,16 +54,3 @@ jobs: test-results/ if-no-files-found: ignore retention-days: 7 - slack-notification: - if: success() || failure() - name: Slack Notification - needs: - - e2e - runs-on: ubuntu-latest - steps: - - name: Slack Notification - uses: Gamesight/slack-workflow-status@master - if: always() - with: - repo_token: ${{ secrets.GITHUB_TOKEN }} - slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }} diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 0181837e93..ce38e51852 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -4,7 +4,9 @@ // List of extensions which should be recommended for users of this workspace. "recommendations": [ "golang.go", - "redhat.vscode-yaml" + "redhat.vscode-yaml", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode" ], // List of extensions recommended by VS Code that should not be recommended for users of this workspace. "unwantedRecommendations": [] diff --git a/.vscode/settings.json b/.vscode/settings.json index ab2e48a8da..7b8d51d19b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,6 @@ { "go.testEnvFile": "${workspaceRoot}/.env", "go.lintTool": "golangci-lint", - "editor.formatOnSave": true, "yaml.format.enable": false, "yaml.completion": true, "yaml.validate": true, @@ -17,20 +16,34 @@ }, "json.schemas": [ { - "fileMatch": [ - "/server/pkg/builtin/manifest.json" - ], + "fileMatch": ["/server/pkg/builtin/manifest.json"], "url": "./server/schemas/plugin_manifest.json" }, { - "fileMatch": [ - "/server/pkg/builtin/manifest_*.json" - ], + "fileMatch": ["/server/pkg/builtin/manifest_*.json"], "url": "./server/schemas/plugin_manifest_translation.json" } ], "[yaml]": { "editor.formatOnSave": false }, - "prettier.enable": false + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "editor.formatOnSave": true, + "eslint.enable": true, + "eslint.useFlatConfig": true, + "prettier.enable": true, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + } } diff --git a/server/Makefile b/server/Makefile index ed74ae7199..ea89ff1aca 100644 --- a/server/Makefile +++ b/server/Makefile @@ -1,3 +1,16 @@ +help: + @echo "Usage:" + @echo " make " + @echo "" + @echo "Targets:" + @echo " lint Run golangci-lint with auto-fix" + @echo " test Run unit tests with race detector in short mode" + @echo " e2e Run end-to-end tests" + @echo " build Build the project" + @echo " run-app Run the application" + @echo " run-db Run the MongoDB database using Docker Compose" + @echo " gql Generate GraphQL code" + lint: golangci-lint run --fix diff --git a/server/e2e/gql_project_test.go b/server/e2e/gql_project_test.go index bf258f9b83..aad2e57ad1 100644 --- a/server/e2e/gql_project_test.go +++ b/server/e2e/gql_project_test.go @@ -4,7 +4,9 @@ import ( "net/http" "testing" + "github.com/gavv/httpexpect/v2" "github.com/reearth/reearth/server/internal/app/config" + "github.com/stretchr/testify/assert" ) func TestCreateProject(t *testing.T) { @@ -195,3 +197,258 @@ func TestSortByName(t *testing.T) { edges.Element(3).Object().Value("node").Object().Value("name").Equal("B-project") edges.Element(4).Object().Value("node").Object().Value("name").Equal(seedProjectName) } + +func TestFindStarredByWorkspace(t *testing.T) { + + e := StartServer(t, &config.Config{ + Origins: []string{"https://example.com"}, + AuthSrv: config.AuthSrvConfig{ + Disabled: true, + }, + }, true, baseSeeder) + project1ID := createProject(e, "Project 1") + project2ID := createProject(e, "Project 2") + project3ID := createProject(e, "Project 3") + project4ID := createProject(e, "Project 4") + + starProject(e, project1ID) + starProject(e, project3ID) + + requestBody := GraphQLRequest{ + OperationName: "GetStarredProjects", + Query: ` + query GetStarredProjects($teamId: ID!) { + starredProjects(teamId: $teamId) { + nodes { + id + name + starred + } + totalCount + } + }`, + Variables: map[string]any{ + "teamId": wID, + }, + } + + response := 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() + + starredProjects := response.Value("data").Object().Value("starredProjects").Object() + + totalCount := starredProjects.Value("totalCount").Raw() + assert.Equal(t, float64(2), totalCount, "Expected 2 starred projects") + + nodes := starredProjects.Value("nodes").Array() + nodeCount := int(nodes.Length().Raw()) + assert.Equal(t, 2, nodeCount, "Expected 2 nodes in the response") + + starredProjectsMap := make(map[string]bool) + for _, node := range nodes.Iter() { + obj := node.Object() + id := obj.Value("id").String().Raw() + name := obj.Value("name").String().Raw() + starred := obj.Value("starred").Boolean().Raw() + + starredProjectsMap[id] = true + + if id == project1ID { + assert.Equal(t, "Project 1", name) + assert.True(t, starred) + } else if id == project3ID { + assert.Equal(t, "Project 3", name) + assert.True(t, starred) + } else { + t.Errorf("Unexpected project in starred projects: %s", id) + } + } + + assert.True(t, starredProjectsMap[project1ID], "Project 1 should be starred") + 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) { + updateProjectMutation := GraphQLRequest{ + OperationName: "UpdateProject", + Query: ` + mutation UpdateProject($projectId: ID!, $starred: Boolean!) { + updateProject(input: {projectId: $projectId, starred: $starred}) { + project { + id + starred + } + } + }`, + Variables: map[string]any{ + "projectId": projectID, + "starred": 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("starred", true) +} + +func TestSortByUpdatedAt(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") + + requestBody := GraphQLRequest{ + OperationName: "UpdateProject", + Query: `mutation UpdateProject($input: UpdateProjectInput!) { + updateProject(input: $input) { + project { + id + name + description + updatedAt + __typename + } + __typename + } + }`, + Variables: map[string]any{ + "input": map[string]any{ + "projectId": project2ID, + "description": "test updaet", + }, + }, + } + + // Update 'project2' + 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!, $pagination: Pagination, $keyword: String, $sort: ProjectSort) { + projects( + teamId: $teamId + 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{ + "pagination": map[string]any{ + "first": 3, // Get first 3 itme + }, + "teamId": wID.String(), + "sort": map[string]string{ + "field": "UPDATEDAT", + "direction": "DESC", // Sort DESC by UPDATEDAT + }, + }, + } + + 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() + + edges.Length().Equal(3) + edges.Element(0).Object().Value("node").Object().Value("name").Equal("project2-test") // 'project2' is first + edges.Element(1).Object().Value("node").Object().Value("name").Equal("project3-test") + edges.Element(2).Object().Value("node").Object().Value("name").Equal("project1-test") +} diff --git a/server/gql/project.graphql b/server/gql/project.graphql index 5673d3e052..42a2245082 100644 --- a/server/gql/project.graphql +++ b/server/gql/project.graphql @@ -124,6 +124,7 @@ type ProjectEdge { extend type Query{ projects(teamId: ID!, includeArchived: Boolean, pagination: Pagination, keyword: String, sort: ProjectSort): ProjectConnection! checkProjectAlias(alias: String!): ProjectAliasAvailability! + starredProjects(teamId: ID!): ProjectConnection! } extend type Mutation { diff --git a/server/gql/workspace.graphql b/server/gql/workspace.graphql index db61e612ed..fa0c64beaf 100644 --- a/server/gql/workspace.graphql +++ b/server/gql/workspace.graphql @@ -25,6 +25,9 @@ type Policy { assetStorageSize: FileSize datasetSchemaCount: Int datasetCount: Int + nlsLayersCount: Int + pageCount: Int + blocksCount: Int } enum Role { diff --git a/server/internal/adapter/gql/generated.go b/server/internal/adapter/gql/generated.go index 8cb40e6b09..eb21021dfc 100644 --- a/server/internal/adapter/gql/generated.go +++ b/server/internal/adapter/gql/generated.go @@ -803,12 +803,15 @@ type ComplexityRoot struct { Policy struct { AssetStorageSize func(childComplexity int) int + BlocksCount func(childComplexity int) int DatasetCount func(childComplexity int) int DatasetSchemaCount func(childComplexity int) int ID func(childComplexity int) int LayerCount func(childComplexity int) int MemberCount func(childComplexity int) int Name func(childComplexity int) int + NlsLayersCount func(childComplexity int) int + PageCount func(childComplexity int) int ProjectCount func(childComplexity int) int PublishedProjectCount func(childComplexity int) int } @@ -1007,6 +1010,7 @@ type ComplexityRoot struct { PropertySchemas func(childComplexity int, id []gqlmodel.ID) int Scene func(childComplexity int, projectID gqlmodel.ID) int SearchUser func(childComplexity int, nameOrEmail string) int + StarredProjects func(childComplexity int, teamID gqlmodel.ID) int } Rect struct { @@ -1686,6 +1690,7 @@ type QueryResolver interface { Plugins(ctx context.Context, id []gqlmodel.ID) ([]*gqlmodel.Plugin, error) Projects(ctx context.Context, teamID gqlmodel.ID, includeArchived *bool, pagination *gqlmodel.Pagination, keyword *string, sort *gqlmodel.ProjectSort) (*gqlmodel.ProjectConnection, error) CheckProjectAlias(ctx context.Context, alias string) (*gqlmodel.ProjectAliasAvailability, error) + StarredProjects(ctx context.Context, teamID gqlmodel.ID) (*gqlmodel.ProjectConnection, error) PropertySchema(ctx context.Context, id gqlmodel.ID) (*gqlmodel.PropertySchema, error) PropertySchemas(ctx context.Context, id []gqlmodel.ID) ([]*gqlmodel.PropertySchema, error) Scene(ctx context.Context, projectID gqlmodel.ID) (*gqlmodel.Scene, error) @@ -5526,6 +5531,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Policy.AssetStorageSize(childComplexity), true + case "Policy.blocksCount": + if e.complexity.Policy.BlocksCount == nil { + break + } + + return e.complexity.Policy.BlocksCount(childComplexity), true + case "Policy.datasetCount": if e.complexity.Policy.DatasetCount == nil { break @@ -5568,6 +5580,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Policy.Name(childComplexity), true + case "Policy.nlsLayersCount": + if e.complexity.Policy.NlsLayersCount == nil { + break + } + + return e.complexity.Policy.NlsLayersCount(childComplexity), true + + case "Policy.pageCount": + if e.complexity.Policy.PageCount == nil { + break + } + + return e.complexity.Policy.PageCount(childComplexity), true + case "Policy.projectCount": if e.complexity.Policy.ProjectCount == nil { break @@ -6624,6 +6650,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Query.SearchUser(childComplexity, args["nameOrEmail"].(string)), true + case "Query.starredProjects": + if e.complexity.Query.StarredProjects == nil { + break + } + + args, err := ec.field_Query_starredProjects_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Query.StarredProjects(childComplexity, args["teamId"].(gqlmodel.ID)), true + case "Rect.east": if e.complexity.Rect.East == nil { break @@ -9529,6 +9567,7 @@ type ProjectEdge { extend type Query{ projects(teamId: ID!, includeArchived: Boolean, pagination: Pagination, keyword: String, sort: ProjectSort): ProjectConnection! checkProjectAlias(alias: String!): ProjectAliasAvailability! + starredProjects(teamId: ID!): ProjectConnection! } extend type Mutation { @@ -10586,6 +10625,9 @@ type Policy { assetStorageSize: FileSize datasetSchemaCount: Int datasetCount: Int + nlsLayersCount: Int + pageCount: Int + blocksCount: Int } enum Role { @@ -12670,6 +12712,21 @@ func (ec *executionContext) field_Query_searchUser_args(ctx context.Context, raw return args, nil } +func (ec *executionContext) field_Query_starredProjects_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 gqlmodel.ID + if tmp, ok := rawArgs["teamId"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("teamId")) + arg0, err = ec.unmarshalNID2githubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐID(ctx, tmp) + if err != nil { + return nil, err + } + } + args["teamId"] = arg0 + return args, nil +} + func (ec *executionContext) field_Scene_datasetSchemas_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -38120,6 +38177,129 @@ func (ec *executionContext) fieldContext_Policy_datasetCount(ctx context.Context return fc, nil } +func (ec *executionContext) _Policy_nlsLayersCount(ctx context.Context, field graphql.CollectedField, obj *gqlmodel.Policy) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Policy_nlsLayersCount(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.NlsLayersCount, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*int) + fc.Result = res + return ec.marshalOInt2ᚖint(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Policy_nlsLayersCount(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Policy", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Policy_pageCount(ctx context.Context, field graphql.CollectedField, obj *gqlmodel.Policy) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Policy_pageCount(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.PageCount, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*int) + fc.Result = res + return ec.marshalOInt2ᚖint(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Policy_pageCount(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Policy", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Policy_blocksCount(ctx context.Context, field graphql.CollectedField, obj *gqlmodel.Policy) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Policy_blocksCount(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.BlocksCount, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*int) + fc.Result = res + return ec.marshalOInt2ᚖint(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Policy_blocksCount(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Policy", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _Polygon_type(ctx context.Context, field graphql.CollectedField, obj *gqlmodel.Polygon) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Polygon_type(ctx, field) if err != nil { @@ -44779,6 +44959,71 @@ func (ec *executionContext) fieldContext_Query_checkProjectAlias(ctx context.Con return fc, nil } +func (ec *executionContext) _Query_starredProjects(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query_starredProjects(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().StarredProjects(rctx, fc.Args["teamId"].(gqlmodel.ID)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*gqlmodel.ProjectConnection) + fc.Result = res + return ec.marshalNProjectConnection2ᚖgithubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐProjectConnection(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Query_starredProjects(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "edges": + return ec.fieldContext_ProjectConnection_edges(ctx, field) + case "nodes": + return ec.fieldContext_ProjectConnection_nodes(ctx, field) + case "pageInfo": + return ec.fieldContext_ProjectConnection_pageInfo(ctx, field) + case "totalCount": + return ec.fieldContext_ProjectConnection_totalCount(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type ProjectConnection", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_starredProjects_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Query_propertySchema(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Query_propertySchema(ctx, field) if err != nil { @@ -52850,6 +53095,12 @@ func (ec *executionContext) fieldContext_Team_policy(ctx context.Context, field return ec.fieldContext_Policy_datasetSchemaCount(ctx, field) case "datasetCount": return ec.fieldContext_Policy_datasetCount(ctx, field) + case "nlsLayersCount": + return ec.fieldContext_Policy_nlsLayersCount(ctx, field) + case "pageCount": + return ec.fieldContext_Policy_pageCount(ctx, field) + case "blocksCount": + return ec.fieldContext_Policy_blocksCount(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Policy", field.Name) }, @@ -70073,6 +70324,12 @@ func (ec *executionContext) _Policy(ctx context.Context, sel ast.SelectionSet, o out.Values[i] = ec._Policy_datasetSchemaCount(ctx, field, obj) case "datasetCount": out.Values[i] = ec._Policy_datasetCount(ctx, field, obj) + case "nlsLayersCount": + out.Values[i] = ec._Policy_nlsLayersCount(ctx, field, obj) + case "pageCount": + out.Values[i] = ec._Policy_pageCount(ctx, field, obj) + case "blocksCount": + out.Values[i] = ec._Policy_blocksCount(ctx, field, obj) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -72201,6 +72458,28 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "starredProjects": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_starredProjects(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) case "propertySchema": field := field diff --git a/server/internal/adapter/gql/gqlmodel/convert_project.go b/server/internal/adapter/gql/gqlmodel/convert_project.go index ae6d8d0c1f..01500b4b70 100644 --- a/server/internal/adapter/gql/gqlmodel/convert_project.go +++ b/server/internal/adapter/gql/gqlmodel/convert_project.go @@ -77,7 +77,7 @@ func ProjectSortTypeFrom(pst *ProjectSort) *project.SortType { case ProjectSortFieldCreatedat: key = "id" case ProjectSortFieldUpdatedat: - key = "updatedAt" + key = "updatedat" case ProjectSortFieldName: key = "name" default: diff --git a/server/internal/adapter/gql/gqlmodel/convert_workspace.go b/server/internal/adapter/gql/gqlmodel/convert_workspace.go index 5b61e48f2a..c008a15c66 100644 --- a/server/internal/adapter/gql/gqlmodel/convert_workspace.go +++ b/server/internal/adapter/gql/gqlmodel/convert_workspace.go @@ -72,5 +72,8 @@ func ToPolicy(p *policy.Policy) *Policy { AssetStorageSize: o.AssetStorageSize, DatasetSchemaCount: o.DatasetSchemaCount, DatasetCount: o.DatasetCount, + NlsLayersCount: o.NLSLayersCount, + PageCount: o.PageCount, + BlocksCount: o.BlocksCount, } } diff --git a/server/internal/adapter/gql/gqlmodel/convert_workspace_test.go b/server/internal/adapter/gql/gqlmodel/convert_workspace_test.go index db73784b27..587a66239a 100644 --- a/server/internal/adapter/gql/gqlmodel/convert_workspace_test.go +++ b/server/internal/adapter/gql/gqlmodel/convert_workspace_test.go @@ -36,6 +36,9 @@ func TestToPolicy(t *testing.T) { AssetStorageSize: lo.ToPtr(int64(5)), DatasetCount: lo.ToPtr(6), DatasetSchemaCount: lo.ToPtr(7), + NlsLayersCount: lo.ToPtr(8), + PageCount: lo.ToPtr(9), + BlocksCount: lo.ToPtr(6), }, ToPolicy(policy.New(policy.Option{ ID: policy.ID("x"), Name: "aaa", @@ -46,6 +49,9 @@ func TestToPolicy(t *testing.T) { AssetStorageSize: lo.ToPtr(int64(5)), DatasetCount: lo.ToPtr(6), DatasetSchemaCount: lo.ToPtr(7), + NLSLayersCount: lo.ToPtr(8), + PageCount: lo.ToPtr(9), + BlocksCount: lo.ToPtr(6), }))) assert.Nil(t, ToPolicy(nil)) } diff --git a/server/internal/adapter/gql/gqlmodel/models_gen.go b/server/internal/adapter/gql/gqlmodel/models_gen.go index a8b7dbf80e..912e509544 100644 --- a/server/internal/adapter/gql/gqlmodel/models_gen.go +++ b/server/internal/adapter/gql/gqlmodel/models_gen.go @@ -1120,6 +1120,9 @@ type Policy struct { AssetStorageSize *int64 `json:"assetStorageSize,omitempty"` DatasetSchemaCount *int `json:"datasetSchemaCount,omitempty"` DatasetCount *int `json:"datasetCount,omitempty"` + NlsLayersCount *int `json:"nlsLayersCount,omitempty"` + PageCount *int `json:"pageCount,omitempty"` + BlocksCount *int `json:"blocksCount,omitempty"` } type Polygon struct { diff --git a/server/internal/adapter/gql/loader_project.go b/server/internal/adapter/gql/loader_project.go index 8ee2684464..8b0c340802 100644 --- a/server/internal/adapter/gql/loader_project.go +++ b/server/internal/adapter/gql/loader_project.go @@ -79,6 +79,28 @@ func (c *ProjectLoader) CheckAlias(ctx context.Context, alias string) (*gqlmodel return &gqlmodel.ProjectAliasAvailability{Alias: alias, Available: ok}, nil } +func (c *ProjectLoader) FindStarredByWorkspace(ctx context.Context, wsID gqlmodel.ID) (*gqlmodel.ProjectConnection, error) { + tid, err := gqlmodel.ToID[accountdomain.Workspace](wsID) + if err != nil { + return nil, err + } + + res, err := c.usecase.FindStarredByWorkspace(ctx, tid, getOperator(ctx)) + if err != nil { + return nil, err + } + + nodes := make([]*gqlmodel.Project, 0, len(res)) + for _, p := range res { + nodes = append(nodes, gqlmodel.ToProject(p)) + } + + return &gqlmodel.ProjectConnection{ + Nodes: nodes, + TotalCount: len(nodes), + }, nil +} + // data loaders type ProjectDataLoader interface { diff --git a/server/internal/adapter/gql/resolver_query.go b/server/internal/adapter/gql/resolver_query.go index a7a4a5885d..2ed6d91967 100644 --- a/server/internal/adapter/gql/resolver_query.go +++ b/server/internal/adapter/gql/resolver_query.go @@ -255,3 +255,7 @@ func (r *queryResolver) SearchUser(ctx context.Context, nameOrEmail string) (*gq func (r *queryResolver) CheckProjectAlias(ctx context.Context, alias string) (*gqlmodel.ProjectAliasAvailability, error) { return loaders(ctx).Project.CheckAlias(ctx, alias) } + +func (r *queryResolver) StarredProjects(ctx context.Context, teamId gqlmodel.ID) (*gqlmodel.ProjectConnection, error) { + return loaders(ctx).Project.FindStarredByWorkspace(ctx, teamId) +} diff --git a/server/internal/infrastructure/memory/nlslayer.go b/server/internal/infrastructure/memory/nlslayer.go index 37bcc93e93..51e95c9e66 100644 --- a/server/internal/infrastructure/memory/nlslayer.go +++ b/server/internal/infrastructure/memory/nlslayer.go @@ -127,6 +127,24 @@ func (r *NLSLayer) FindNLSLayerSimpleByID(ctx context.Context, id id.NLSLayerID) return nil, rerror.ErrNotFound } +func (r *NLSLayer) CountByScene(_ context.Context, sid id.SceneID) (n int, _ error) { + r.lock.Lock() + defer r.lock.Unlock() + + if !r.f.CanRead(sid) { + return + } + + for _, d := range r.data { + if d.Scene() == sid { + if lg := nlslayer.ToNLSLayerGroup(d); lg == nil && !lg.IsRoot() { + n++ + } + } + } + return +} + func (r *NLSLayer) Save(ctx context.Context, l nlslayer.NLSLayer) error { if !r.f.CanWrite(l.Scene()) { return repo.ErrOperationDenied diff --git a/server/internal/infrastructure/memory/project.go b/server/internal/infrastructure/memory/project.go index b4da73f168..13688864f3 100644 --- a/server/internal/infrastructure/memory/project.go +++ b/server/internal/infrastructure/memory/project.go @@ -83,6 +83,28 @@ func (r *Project) FindByWorkspace(ctx context.Context, id accountdomain.Workspac ), nil } +func (r *Project) FindStarredByWorkspace(ctx context.Context, id accountdomain.WorkspaceID) ([]*project.Project, error) { + r.lock.Lock() + defer r.lock.Unlock() + + if !r.f.CanRead(id) { + return nil, nil + } + + var result []*project.Project + for _, p := range r.data { + if p.Workspace() == id && p.Starred() { + result = append(result, p) + } + } + + sort.Slice(result, func(i, j int) bool { + return result[i].UpdatedAt().After(result[j].UpdatedAt()) + }) + + return result, nil +} + func (r *Project) FindIDsByWorkspace(ctx context.Context, id accountdomain.WorkspaceID) (res []project.ID, _ error) { r.lock.Lock() defer r.lock.Unlock() diff --git a/server/internal/infrastructure/mongo/migration/migrations.go b/server/internal/infrastructure/mongo/migration/migrations.go index e6ccbc8e35..41741bbf98 100644 --- a/server/internal/infrastructure/mongo/migration/migrations.go +++ b/server/internal/infrastructure/mongo/migration/migrations.go @@ -10,11 +10,11 @@ import "github.com/reearth/reearthx/usecasex/migration" // If the migration takes too long, the deployment may fail in a serverless environment. // Set the batch size to as large a value as possible without using up the RAM of the deployment destination. var migrations = migration.Migrations[DBClient]{ - 201217132559: AddSceneWidgetId, - 201217193948: AddSceneDefaultTile, - 210310145844: RemovePreviewToken, - 210730175108: AddSceneAlignSystem, - 220214180713: SplitSchemaOfProperties, - 220309174648: AddSceneFieldToPropertySchema, - 221028204300: MoveTerrainProperties, + 201217132559: AddSceneWidgetId, + 201217193948: AddSceneDefaultTile, + 210310145844: RemovePreviewToken, + 210730175108: AddSceneAlignSystem, + 220214180713: SplitSchemaOfProperties, + 220309174648: AddSceneFieldToPropertySchema, + 221028204300: MoveTerrainProperties, } diff --git a/server/internal/infrastructure/mongo/mongodoc/config.go b/server/internal/infrastructure/mongo/mongodoc/config.go index b12f0755e7..ac73560322 100644 --- a/server/internal/infrastructure/mongo/mongodoc/config.go +++ b/server/internal/infrastructure/mongo/mongodoc/config.go @@ -18,8 +18,9 @@ type Auth struct { func NewConfig(c config.Config) ConfigDocument { return ConfigDocument{ - Migration: c.Migration, - Auth: NewConfigAuth(c.Auth), + Migration: c.Migration, + Auth: NewConfigAuth(c.Auth), + DefaultPolicy: c.DefaultPolicy, } } @@ -39,7 +40,8 @@ func (c *ConfigDocument) Model() *config.Config { } cfg := &config.Config{ - Migration: c.Migration, + Migration: c.Migration, + DefaultPolicy: c.DefaultPolicy, } if c.Auth != nil { diff --git a/server/internal/infrastructure/mongo/mongodoc/policy.go b/server/internal/infrastructure/mongo/mongodoc/policy.go index 346bd7d590..838e9a926e 100644 --- a/server/internal/infrastructure/mongo/mongodoc/policy.go +++ b/server/internal/infrastructure/mongo/mongodoc/policy.go @@ -15,6 +15,9 @@ type PolicyDocument struct { DatasetCount *int DatasetSchemaCount *int AssetStorageSize *int64 + NLSLayersCount *int + PageCount *int + BlocksCount *int } func (d PolicyDocument) Model() *policy.Policy { @@ -28,6 +31,9 @@ func (d PolicyDocument) Model() *policy.Policy { DatasetCount: d.DatasetCount, DatasetSchemaCount: d.DatasetSchemaCount, AssetStorageSize: d.AssetStorageSize, + NLSLayersCount: d.NLSLayersCount, + PageCount: d.PageCount, + BlocksCount: d.BlocksCount, }) } diff --git a/server/internal/infrastructure/mongo/nlslayer.go b/server/internal/infrastructure/mongo/nlslayer.go index 73c7281bef..4203b6434f 100644 --- a/server/internal/infrastructure/mongo/nlslayer.go +++ b/server/internal/infrastructure/mongo/nlslayer.go @@ -136,6 +136,18 @@ func (r *NLSLayer) FindByScene(ctx context.Context, id id.SceneID) (nlslayer.NLS }) } +func (r *NLSLayer) CountByScene(ctx context.Context, sid id.SceneID) (int, error) { + if !r.f.CanRead(sid) { + return 0, repo.ErrOperationDenied + } + + c, err := r.client.Count(ctx, bson.M{ + "scene": sid.String(), + "group.root": bson.M{"$ne": true}, + }) + return int(c), err +} + func (r *NLSLayer) Save(ctx context.Context, layer nlslayer.NLSLayer) error { if !r.f.CanWrite(layer.Scene()) { return repo.ErrOperationDenied diff --git a/server/internal/infrastructure/mongo/project.go b/server/internal/infrastructure/mongo/project.go index 6822048b0b..cda562ed01 100644 --- a/server/internal/infrastructure/mongo/project.go +++ b/server/internal/infrastructure/mongo/project.go @@ -96,6 +96,19 @@ func (r *Project) FindByWorkspace(ctx context.Context, id accountdomain.Workspac return r.paginate(ctx, filter, uFilter.Sort, uFilter.Pagination) } +func (r *Project) FindStarredByWorkspace(ctx context.Context, id accountdomain.WorkspaceID) ([]*project.Project, error) { + if !r.f.CanRead(id) { + return nil, repo.ErrOperationDenied + } + + filter := bson.M{ + "team": id.String(), + "starred": true, + } + + return r.find(ctx, filter) +} + func (r *Project) FindByPublicName(ctx context.Context, name string) (*project.Project, error) { if name == "" { return nil, rerror.ErrNotFound diff --git a/server/internal/infrastructure/mongo/project_test.go b/server/internal/infrastructure/mongo/project_test.go index e3eb604737..254108b3ce 100644 --- a/server/internal/infrastructure/mongo/project_test.go +++ b/server/internal/infrastructure/mongo/project_test.go @@ -139,3 +139,55 @@ func TestProject_FindByPublicName(t *testing.T) { assert.NoError(t, err) assert.Equal(t, prj1, got) } + +func TestProject_FindStarredByWorkspace(t *testing.T) { + c := mongotest.Connect(t)(t) + ctx := context.Background() + + wid := accountdomain.NewWorkspaceID() + wid2 := accountdomain.NewWorkspaceID() + + pid1 := id.NewProjectID() + pid2 := id.NewProjectID() + pid3 := id.NewProjectID() + pid4 := id.NewProjectID() + + _, _ = c.Collection("project").InsertMany(ctx, []any{ + bson.M{"id": pid1.String(), "team": wid.String(), "name": "Project 1", "starred": true}, + bson.M{"id": pid2.String(), "team": wid.String(), "name": "Project 2", "starred": true}, + bson.M{"id": pid3.String(), "team": wid.String(), "name": "Project 3", "starred": false}, + bson.M{"id": pid4.String(), "team": wid2.String(), "name": "Project 4", "starred": true}, + }) + + r := NewProject(mongox.NewClientWithDatabase(c)) + + t.Run("FindStarredByWorkspace", func(t *testing.T) { + got, err := r.FindStarredByWorkspace(ctx, wid) + assert.NoError(t, err) + assert.Equal(t, 2, len(got)) + assert.ElementsMatch(t, []id.ProjectID{pid1, pid2}, []id.ProjectID{got[0].ID(), got[1].ID()}) + }) + + t.Run("FindStarredByWorkspace with workspace filter", func(t *testing.T) { + r2 := r.Filtered(repo.WorkspaceFilter{ + Readable: accountdomain.WorkspaceIDList{wid2}, + }) + got, err := r2.FindStarredByWorkspace(ctx, wid) + assert.Equal(t, repo.ErrOperationDenied, err) + assert.Nil(t, got) + }) + + t.Run("FindStarredByWorkspace with different workspace", func(t *testing.T) { + got, err := r.FindStarredByWorkspace(ctx, wid2) + assert.NoError(t, err) + assert.Equal(t, 1, len(got)) + assert.Equal(t, pid4, got[0].ID()) + }) + + t.Run("FindStarredByWorkspace with workspace having no starred projects", func(t *testing.T) { + emptyWid := accountdomain.NewWorkspaceID() + got, err := r.FindStarredByWorkspace(ctx, emptyWid) + assert.NoError(t, err) + assert.Empty(t, got) + }) +} diff --git a/server/internal/usecase/interactor/nlslayer.go b/server/internal/usecase/interactor/nlslayer.go index 689c10341a..5b0d7c43b6 100644 --- a/server/internal/usecase/interactor/nlslayer.go +++ b/server/internal/usecase/interactor/nlslayer.go @@ -13,6 +13,7 @@ import ( "github.com/reearth/reearth/server/pkg/nlslayer/nlslayerops" "github.com/reearth/reearth/server/pkg/plugin" "github.com/reearth/reearth/server/pkg/property" + "github.com/reearth/reearthx/account/accountusecase/accountrepo" "github.com/reearth/reearthx/rerror" "github.com/reearth/reearthx/usecasex" ) @@ -26,6 +27,8 @@ type NLSLayer struct { sceneRepo repo.Scene propertyRepo repo.Property pluginRepo repo.Plugin + policyRepo repo.Policy + workspaceRepo accountrepo.Workspace transaction usecasex.Transaction } @@ -38,6 +41,8 @@ func NewNLSLayer(r *repo.Container) interfaces.NLSLayer { sceneRepo: r.Scene, propertyRepo: r.Property, pluginRepo: r.Plugin, + policyRepo: r.Policy, + workspaceRepo: r.Workspace, transaction: r.Transaction, } } @@ -87,6 +92,30 @@ func (i *NLSLayer) AddLayerSimple(ctx context.Context, inp interfaces.AddNLSLaye return nil, err } + s, err := i.sceneRepo.FindByID(ctx, inp.SceneID) + if err != nil { + return nil, err + } + + ws, err := i.workspaceRepo.FindByID(ctx, s.Workspace()) + if err != nil { + return nil, err + } + + if policyID := operator.Policy(ws.Policy()); policyID != nil { + p, err := i.policyRepo.FindByID(ctx, *policyID) + if err != nil { + return nil, err + } + s, err := i.nlslayerRepo.CountByScene(ctx, s.ID()) + if err != nil { + return nil, err + } + if err := p.EnforceNLSLayersCount(s + 1); err != nil { + return nil, err + } + } + if inp.Schema != nil { featureCollection := nlslayer.NewFeatureCollection( "FeatureCollection", diff --git a/server/internal/usecase/interactor/project.go b/server/internal/usecase/interactor/project.go index 03befee355..d42c60a4cf 100644 --- a/server/internal/usecase/interactor/project.go +++ b/server/internal/usecase/interactor/project.go @@ -73,6 +73,10 @@ func (i *Project) FindByWorkspace(ctx context.Context, id accountdomain.Workspac }) } +func (i *Project) FindStarredByWorkspace(ctx context.Context, id accountdomain.WorkspaceID, operator *usecase.Operator) ([]*project.Project, error) { + return i.projectRepo.FindStarredByWorkspace(ctx, id) +} + func (i *Project) Create(ctx context.Context, p interfaces.CreateProjectParam, operator *usecase.Operator) (_ *project.Project, err error) { if err := i.CanWriteWorkspace(p.WorkspaceID, operator); err != nil { return nil, err diff --git a/server/internal/usecase/interactor/storytelling.go b/server/internal/usecase/interactor/storytelling.go index b3a3b09809..4424644b0f 100644 --- a/server/internal/usecase/interactor/storytelling.go +++ b/server/internal/usecase/interactor/storytelling.go @@ -464,6 +464,28 @@ func (i *Storytelling) CreatePage(ctx context.Context, inp interfaces.CreatePage return nil, nil, err } + s, err := i.sceneRepo.FindByID(ctx, inp.SceneID) + if err != nil { + return nil, nil, err + } + + ws, err := i.workspaceRepo.FindByID(ctx, s.Workspace()) + if err != nil { + return nil, nil, err + } + + if policyID := op.Policy(ws.Policy()); policyID != nil { + p, err := i.policyRepo.FindByID(ctx, *policyID) + if err != nil { + return nil, nil, err + } + + var pageCount = len(story.Pages().Pages()) + if err := p.EnforcePageCount(pageCount + 1); err != nil { + return nil, nil, err + } + } + story.Pages().AddAt(page, inp.Index) if err = i.propertyRepo.Save(ctx, prop); err != nil { @@ -789,6 +811,38 @@ func (i *Storytelling) CreateBlock(ctx context.Context, inp interfaces.CreateBlo return nil, nil, nil, -1, err } + s, err := i.sceneRepo.FindByID(ctx, story.Scene()) + if err != nil { + return nil, nil, nil, -1, err + } + + ws, err := i.workspaceRepo.FindByID(ctx, s.Workspace()) + if err != nil { + return nil, nil, nil, -1, err + } + + if policyID := op.Policy(ws.Policy()); policyID != nil { + p, err := i.policyRepo.FindByID(ctx, *policyID) + if err != nil { + return nil, nil, nil, -1, err + } + + story, err := i.storytellingRepo.FindByID(ctx, inp.StoryID) + if err != nil { + return nil, nil, nil, -1, err + } + + page := story.Pages().Page(inp.PageID) + if page == nil { + return nil, nil, nil, -1, interfaces.ErrPageNotFound + } + + var s = page.Count() + if err := p.EnforceBlocksCount(s + 1); err != nil { + return nil, nil, nil, -1, err + } + } + _, extension, err := i.getPlugin(ctx, &inp.PluginID, &inp.ExtensionID) if err != nil { return nil, nil, nil, -1, err diff --git a/server/internal/usecase/interfaces/project.go b/server/internal/usecase/interfaces/project.go index 0f0b9a338f..7bfc578f3a 100644 --- a/server/internal/usecase/interfaces/project.go +++ b/server/internal/usecase/interfaces/project.go @@ -60,6 +60,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) + 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) Publish(context.Context, PublishProjectParam, *usecase.Operator) (*project.Project, error) diff --git a/server/internal/usecase/repo/nlslayer.go b/server/internal/usecase/repo/nlslayer.go index 5fca222601..44ca883ab0 100644 --- a/server/internal/usecase/repo/nlslayer.go +++ b/server/internal/usecase/repo/nlslayer.go @@ -18,6 +18,7 @@ type NLSLayer interface { FindParentByID(context.Context, id.NLSLayerID) (*nlslayer.NLSLayerGroup, error) FindParentsByIDs(context.Context, id.NLSLayerIDList) (nlslayer.NLSLayerGroupList, error) FindByScene(context.Context, id.SceneID) (nlslayer.NLSLayerList, error) + CountByScene(context.Context, id.SceneID) (int, error) Save(context.Context, nlslayer.NLSLayer) error SaveAll(context.Context, nlslayer.NLSLayerList) error Remove(context.Context, id.NLSLayerID) error diff --git a/server/internal/usecase/repo/project.go b/server/internal/usecase/repo/project.go index f72c44babd..7ba88d180d 100644 --- a/server/internal/usecase/repo/project.go +++ b/server/internal/usecase/repo/project.go @@ -22,6 +22,7 @@ type Project interface { FindByID(context.Context, id.ProjectID) (*project.Project, error) FindByScene(context.Context, id.SceneID) (*project.Project, error) FindByWorkspace(context.Context, accountdomain.WorkspaceID, ProjectFilter) ([]*project.Project, *usecasex.PageInfo, error) + FindStarredByWorkspace(context.Context, accountdomain.WorkspaceID) ([]*project.Project, error) FindByPublicName(context.Context, string) (*project.Project, error) CountByWorkspace(context.Context, accountdomain.WorkspaceID) (int, error) CountPublicByWorkspace(context.Context, accountdomain.WorkspaceID) (int, error) diff --git a/server/pkg/builtin/manifest.yml b/server/pkg/builtin/manifest.yml index c37e6e02a5..6bcf6d0865 100644 --- a/server/pkg/builtin/manifest.yml +++ b/server/pkg/builtin/manifest.yml @@ -2709,3 +2709,45 @@ extensions: field: displayType type: string value: custom + - id: videoInfoboxBetaBlock + name: Video + type: infoboxBlock + description: Infobox Video Block + schema: + groups: + - id: panel + title: Panel Setting + fields: + - id: padding + type: spacing + title: Padding + ui: padding + min: 0 + max: 100 + - id: default + title: Video Block + fields: + - id: src + type: string + title: Video + - id: markdownInfoboxBetaBlock + name: MD Text + type: infoboxBlock + description: Infobox MD Text Block + schema: + groups: + - id: panel + title: Panel Setting + fields: + - id: padding + type: spacing + title: Padding + ui: padding + min: 0 + max: 100 + - id: default + title: MD Text Block + fields: + - id: src + type: string + title: Content diff --git a/server/pkg/builtin/manifest_ja.yml b/server/pkg/builtin/manifest_ja.yml index 7c54b474e2..cd6bfaf32c 100644 --- a/server/pkg/builtin/manifest_ja.yml +++ b/server/pkg/builtin/manifest_ja.yml @@ -1347,6 +1347,34 @@ extensions: custom: カスタム property_list: title: プロパティリスト + panel: + title: パネル設定 + fields: + padding: + title: 余白 + videoInfoboxBetaBlock: + name: ビデオ + description: インフォボックスビデオブロック + propertySchema: + default: + title: ビデオブロック + fields: + src: + title: ビデオ + panel: + title: パネル設定 + fields: + padding: + title: 余白 + markdownInfoboxBetaBlock: + name: MDテキスト + description: インフォボックスMDテキストブロック + propertySchema: + default: + title: MDテキストブロック + fields: + src: + title: コンテンツ panel: title: パネル設定 fields: diff --git a/server/pkg/config/config.go b/server/pkg/config/config.go index d5745c3176..9c005dc8a1 100644 --- a/server/pkg/config/config.go +++ b/server/pkg/config/config.go @@ -2,11 +2,14 @@ package config import ( "sort" + + "github.com/reearth/reearth/server/pkg/policy" ) type Config struct { - Migration int64 - Auth *Auth + Migration int64 + Auth *Auth + DefaultPolicy *policy.ID } type Auth struct { diff --git a/server/pkg/policy/policy.go b/server/pkg/policy/policy.go index eede59af53..17c68c0849 100644 --- a/server/pkg/policy/policy.go +++ b/server/pkg/policy/policy.go @@ -25,6 +25,9 @@ type Option struct { AssetStorageSize *int64 DatasetSchemaCount *int DatasetCount *int + NLSLayersCount *int + PageCount *int + BlocksCount *int } func New(opts Option) *Policy { @@ -71,6 +74,18 @@ func (p *Policy) EnforceDatasetCount(count int) error { return p.error(p == nil || p.opts.DatasetCount == nil || *p.opts.DatasetCount >= count) } +func (p *Policy) EnforceNLSLayersCount(count int) error { + return p.error(p == nil || p.opts.NLSLayersCount == nil || *p.opts.NLSLayersCount >= count) +} + +func (p *Policy) EnforcePageCount(count int) error { + return p.error(p == nil || p.opts.PageCount == nil || *p.opts.PageCount >= count) +} + +func (p *Policy) EnforceBlocksCount(count int) error { + return p.error(p == nil || p.opts.BlocksCount == nil || *p.opts.BlocksCount >= count) +} + func (*Policy) error(ok bool) error { if !ok { return ErrPolicyViolation @@ -96,5 +111,8 @@ func (p Option) Clone() Option { AssetStorageSize: util.CloneRef(p.AssetStorageSize), DatasetSchemaCount: util.CloneRef(p.DatasetSchemaCount), DatasetCount: util.CloneRef(p.DatasetCount), + NLSLayersCount: util.CloneRef(p.NLSLayersCount), + PageCount: util.CloneRef(p.PageCount), + BlocksCount: util.CloneRef(p.BlocksCount), } } diff --git a/server/pkg/policy/policy_test.go b/server/pkg/policy/policy_test.go index 3fe0bcdb54..373865a6f9 100644 --- a/server/pkg/policy/policy_test.go +++ b/server/pkg/policy/policy_test.go @@ -150,6 +150,57 @@ func TestPolicy_EnforceDatasetCount(t *testing.T) { }) } +func TestPolicy_EnforceNLSLayersCount(t *testing.T) { + tests := []policyTest[int]{ + {limit: 0, arg: 0, fail: false}, + {limit: 1, arg: 0, fail: false}, + {limit: 1, arg: 1, fail: false}, + {limit: 1, arg: 2, fail: true}, + {limitNil: true, arg: 100, fail: false}, + {policyNil: true, arg: 100, fail: false}, + } + + testPolicy(t, tests, func(d int) Option { + return Option{NLSLayersCount: lo.ToPtr(d)} + }, func(p *Policy, a int) error { + return p.EnforceNLSLayersCount(a) + }) +} + +func TestPolicy_EnforcePageCount(t *testing.T) { + tests := []policyTest[int]{ + {limit: 0, arg: 0, fail: false}, + {limit: 1, arg: 0, fail: false}, + {limit: 1, arg: 1, fail: false}, + {limit: 1, arg: 2, fail: true}, + {limitNil: true, arg: 100, fail: false}, + {policyNil: true, arg: 100, fail: false}, + } + + testPolicy(t, tests, func(d int) Option { + return Option{PageCount: lo.ToPtr(d)} + }, func(p *Policy, a int) error { + return p.EnforcePageCount(a) + }) +} + +func TestPolicy_EnforceBlocksCount(t *testing.T) { + tests := []policyTest[int]{ + {limit: 0, arg: 0, fail: false}, + {limit: 1, arg: 0, fail: false}, + {limit: 1, arg: 1, fail: false}, + {limit: 1, arg: 2, fail: true}, + {limitNil: true, arg: 100, fail: false}, + {policyNil: true, arg: 100, fail: false}, + } + + testPolicy(t, tests, func(d int) Option { + return Option{BlocksCount: lo.ToPtr(d)} + }, func(p *Policy, a int) error { + return p.EnforceBlocksCount(a) + }) +} + func testPolicy[T any](t *testing.T, tests []policyTest[T], f func(d T) Option, tf func(p *Policy, a T) error) { t.Helper() for _, tt := range tests { @@ -191,7 +242,10 @@ func TestPolicy_Clone(t *testing.T) { LayerCount: lo.ToPtr(1), AssetStorageSize: lo.ToPtr(int64(1)), DatasetSchemaCount: lo.ToPtr(1), - DatasetCount: lo.ToPtr(2), + DatasetCount: lo.ToPtr(1), + NLSLayersCount: lo.ToPtr(1), + PageCount: lo.ToPtr(1), + BlocksCount: lo.ToPtr(1), }, } got := p.Clone() diff --git a/server/schemas/plugin_manifest.json b/server/schemas/plugin_manifest.json index bcb2bed9a7..6cdbf013c4 100644 --- a/server/schemas/plugin_manifest.json +++ b/server/schemas/plugin_manifest.json @@ -336,7 +336,8 @@ "cluster", "story", "storyPage", - "storyBlock" + "storyBlock", + "infoboxBlock" ] }, "singleOnly": { @@ -454,4 +455,4 @@ } }, "$ref": "#/definitions/root" -} +} \ No newline at end of file diff --git a/web/.env.example b/web/.env.example new file mode 100644 index 0000000000..2ef5020335 --- /dev/null +++ b/web/.env.example @@ -0,0 +1,9 @@ +# Required +REEARTH_WEB_API=http://localhost:8080/api +REEARTH_WEB_PLUGINS=http://localhost:8080/plugins +REEARTH_WEB_AUTH0_DOMAIN= +REEARTH_WEB_AUTH0_AUDIENCE= +REEARTH_WEB_AUTH0_CLIENT_ID= + +# Optional +REEARTH_WEB_CESIUM_ION_TOKEN_URL= \ No newline at end of file diff --git a/web/.eslintignore b/web/.eslintignore deleted file mode 100644 index 95ced518e0..0000000000 --- a/web/.eslintignore +++ /dev/null @@ -1,7 +0,0 @@ -/build -/dist -/coverage -/storybook-static -!/.storybook -/.storybook/public -/src/services/gql/__gen__ \ No newline at end of file diff --git a/web/.eslintrc.yml b/web/.eslintrc.yml deleted file mode 100644 index 0102080ce3..0000000000 --- a/web/.eslintrc.yml +++ /dev/null @@ -1,42 +0,0 @@ -extends: - - reearth -root: true -rules: - no-restricted-imports: - - error - - paths: - - name: lodash - message: Use lodash-es instead. - import/order: - - warn - - pathGroups: - - pattern: "@reearth/**" - group: external - position: after - pathGroupsExcludedImportTypes: ["builtin"] - newlines-between: always - alphabetize: - order: asc - caseInsensitive: true -overrides: - - files: - - e2e/**/* - extends: - - reearth/node - - plugin:playwright/playwright-test - env: - browser: false - rules: - no-restricted-imports: - - error - - paths: - - name: "@playwright/test" - message: Use @reearth/e2e/utils instead. - - files: - - i18next-parser.config.js - extends: - - reearth/commonjs - - files: src/beta/** - rules: - "@typescript-eslint/no-explicit-any": - - warn diff --git a/web/.gitignore b/web/.gitignore index 04bcd77583..9662ac8a30 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -9,7 +9,7 @@ node_modules __screenshots__ /playwright-report /test-results -/.env* +/.env /reearth-config.json /cesium_ion_token.txt .idea/* diff --git a/web/.huskyrc.json b/web/.huskyrc.json index 05ea0d92bf..4d077c8292 100644 --- a/web/.huskyrc.json +++ b/web/.huskyrc.json @@ -2,4 +2,4 @@ "hooks": { "pre-commit": "lint-staged" } -} \ No newline at end of file +} diff --git a/web/.prettierignore b/web/.prettierignore index ec6d3cdd7f..d85205ac1d 100644 --- a/web/.prettierignore +++ b/web/.prettierignore @@ -1 +1,3 @@ package.json +**/__gen__/** +src/services/i18n/translations/*.yml \ No newline at end of file diff --git a/web/.prettierrc b/web/.prettierrc new file mode 100644 index 0000000000..36b356317b --- /dev/null +++ b/web/.prettierrc @@ -0,0 +1,3 @@ +{ + "trailingComma": "none" +} diff --git a/web/.storybook/main.ts b/web/.storybook/main.ts index 91acbc0d14..77251d12f7 100644 --- a/web/.storybook/main.ts +++ b/web/.storybook/main.ts @@ -8,55 +8,55 @@ const config: StorybookConfig = { addons: ["@storybook/addon-essentials", "@storybook/addon-styling"], framework: { name: "@storybook/react-vite", - options: {}, + options: {} }, core: { - disableTelemetry: true, + disableTelemetry: true }, staticDirs: ["./public"], viteFinal(config, { configType }) { return mergeConfig(config, { define: { - "process.env.QTS_DEBUG": "false", // quickjs-emscripten + "process.env.QTS_DEBUG": "false" // quickjs-emscripten }, build: configType === "PRODUCTION" ? { // https://github.com/storybookjs/builder-vite/issues/409 minify: false, - sourcemap: false, + sourcemap: false } : {}, resolve: { alias: [ { find: "crypto", - replacement: "crypto-js", + replacement: "crypto-js" }, { find: "@reearth/core", - replacement: resolve(__dirname, "..", "node_modules/@reearth/core"), + replacement: resolve(__dirname, "..", "node_modules/@reearth/core") }, // quickjs-emscripten { find: "@reearth", - replacement: resolve(__dirname, "..", "src"), - }, - ], + replacement: resolve(__dirname, "..", "src") + } + ] }, server: { watch: { // https://github.com/storybookjs/storybook/issues/22253#issuecomment-1673229400 - ignored: ["**/.env"], - }, + ignored: ["**/.env"] + } }, optimizeDeps: { - exclude: ["storybook"], - }, + exclude: ["storybook"] + } }); }, docs: { - autodocs: true, - }, + autodocs: true + } }; module.exports = config; diff --git a/web/.storybook/manager.ts b/web/.storybook/manager.ts index 3ea19f8bdb..0376b21922 100644 --- a/web/.storybook/manager.ts +++ b/web/.storybook/manager.ts @@ -3,5 +3,5 @@ import { addons } from "@storybook/manager-api"; import theme from "./theme"; addons.setConfig({ - theme, + theme }); diff --git a/web/.storybook/preview-head.html b/web/.storybook/preview-head.html index bc16b3abc1..c6ce14c850 100644 --- a/web/.storybook/preview-head.html +++ b/web/.storybook/preview-head.html @@ -1,7 +1,10 @@ - \ No newline at end of file + diff --git a/web/.storybook/preview.tsx b/web/.storybook/preview.tsx index 215f8c77d3..7feb977a8a 100644 --- a/web/.storybook/preview.tsx +++ b/web/.storybook/preview.tsx @@ -1,10 +1,9 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ import { ApolloProvider, ApolloClient, InMemoryCache, ApolloLink, - Observable, + Observable } from "@apollo/client"; import { ThemeProvider } from "@emotion/react"; import { withThemeFromJSXProvider } from "@storybook/addon-styling"; @@ -21,11 +20,11 @@ import theme from "./theme"; const mockClient = new ApolloClient({ link: new ApolloLink( () => - new Observable(observer => { + new Observable((observer) => { observer.complete(); - }), + }) ), - cache: new InMemoryCache(), + cache: new InMemoryCache() }); const preview: Preview = { @@ -33,27 +32,27 @@ const preview: Preview = { backgrounds: { values: [ { name: "Light", value: "lightGrey" }, - { name: "Dark", value: "ash" }, - ], + { name: "Dark", value: "ash" } + ] }, layout: "fullscreen", controls: { expanded: true }, actions: { argTypesRegex: "^on.*" }, docs: { - theme, - }, + theme + } }, decorators: [ withThemeFromJSXProvider({ themes: { light: lightTheme, - dark: darkTheme, + dark: darkTheme }, defaultTheme: "dark", Provider: ThemeProvider, - GlobalStyles, + GlobalStyles }), - Story => { + (Story) => { return ( @@ -63,8 +62,8 @@ const preview: Preview = { ); - }, - ], + } + ] }; export default preview; diff --git a/web/.storybook/public/plugins/block.js b/web/.storybook/public/plugins/block.js index 75b4e57beb..cf203dd3d1 100644 --- a/web/.storybook/public/plugins/block.js +++ b/web/.storybook/public/plugins/block.js @@ -34,6 +34,6 @@ reearth.ui.show(html); reearth.on("update", () => { reearth.ui.postMessage(reearth.block); }); -reearth.on("message", msg => { +reearth.on("message", (msg) => { console.log("message received:", msg); }); diff --git a/web/.storybook/public/sample.geojson b/web/.storybook/public/sample.geojson index 5593349c59..8a8185bbcf 100644 --- a/web/.storybook/public/sample.geojson +++ b/web/.storybook/public/sample.geojson @@ -5,10 +5,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -47.18951431751117, - -24.789938520384457 - ] + "coordinates": [-47.18951431751117, -24.789938520384457] }, "properties": {} }, @@ -16,10 +13,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 155.84221983372979, - 74.01264716547318 - ] + "coordinates": [155.84221983372979, 74.01264716547318] }, "properties": {} }, @@ -27,10 +21,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 110.07076785868927, - -89.46999216488513 - ] + "coordinates": [110.07076785868927, -89.46999216488513] }, "properties": {} }, @@ -38,10 +29,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -99.86847083205676, - 63.7797307132169 - ] + "coordinates": [-99.86847083205676, 63.7797307132169] }, "properties": {} }, @@ -49,10 +37,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -113.37578194870969, - -76.69367044221802 - ] + "coordinates": [-113.37578194870969, -76.69367044221802] }, "properties": {} }, @@ -60,10 +45,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 72.72561073468972, - -66.07423396540386 - ] + "coordinates": [72.72561073468972, -66.07423396540386] }, "properties": {} }, @@ -71,10 +53,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -40.760352456527926, - 35.2020395893229 - ] + "coordinates": [-40.760352456527926, 35.2020395893229] }, "properties": {} }, @@ -82,10 +61,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 64.5926351271439, - -44.17513767822357 - ] + "coordinates": [64.5926351271439, -44.17513767822357] }, "properties": {} }, @@ -93,10 +69,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -42.09615220517878, - -33.50335584434493 - ] + "coordinates": [-42.09615220517878, -33.50335584434493] }, "properties": {} }, @@ -104,10 +77,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -149.22492792801265, - 21.841224879347543 - ] + "coordinates": [-149.22492792801265, 21.841224879347543] }, "properties": {} }, @@ -115,10 +85,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 84.29321157015941, - 0.07515859608883346 - ] + "coordinates": [84.29321157015941, 0.07515859608883346] }, "properties": {} }, @@ -126,10 +93,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -78.3095833369632, - 4.117647950235579 - ] + "coordinates": [-78.3095833369632, 4.117647950235579] }, "properties": {} }, @@ -137,10 +101,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -148.5588515858767, - -63.777936872357444 - ] + "coordinates": [-148.5588515858767, -63.777936872357444] }, "properties": {} }, @@ -148,10 +109,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -100.10454457712412, - 86.03524243483149 - ] + "coordinates": [-100.10454457712412, 86.03524243483149] }, "properties": {} }, @@ -159,10 +117,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 13.436120209269031, - 82.41150556167928 - ] + "coordinates": [13.436120209269031, 82.41150556167928] }, "properties": {} }, @@ -170,10 +125,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 124.28432367091438, - 8.144080311051605 - ] + "coordinates": [124.28432367091438, 8.144080311051605] }, "properties": {} }, @@ -181,10 +133,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -173.04327936439537, - -72.49509733559518 - ] + "coordinates": [-173.04327936439537, -72.49509733559518] }, "properties": {} }, @@ -192,10 +141,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -96.18937306090523, - -57.13860396512657 - ] + "coordinates": [-96.18937306090523, -57.13860396512657] }, "properties": {} }, @@ -203,10 +149,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 94.57455414504705, - 68.25466741220373 - ] + "coordinates": [94.57455414504705, 68.25466741220373] }, "properties": {} }, @@ -214,10 +157,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 81.74710445221764, - -70.17136693930061 - ] + "coordinates": [81.74710445221764, -70.17136693930061] }, "properties": {} }, @@ -225,10 +165,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -175.60441108831705, - -5.460861619300985 - ] + "coordinates": [-175.60441108831705, -5.460861619300985] }, "properties": {} }, @@ -236,10 +173,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -141.4436798546378, - 73.45550273035175 - ] + "coordinates": [-141.4436798546378, 73.45550273035175] }, "properties": {} }, @@ -247,10 +181,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -146.00894799433456, - 68.81252435965777 - ] + "coordinates": [-146.00894799433456, 68.81252435965777] }, "properties": {} }, @@ -258,10 +189,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -141.1682273861793, - -75.59589534868311 - ] + "coordinates": [-141.1682273861793, -75.59589534868311] }, "properties": {} }, @@ -269,10 +197,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -65.48510873602658, - 63.81204037415824 - ] + "coordinates": [-65.48510873602658, 63.81204037415824] }, "properties": {} }, @@ -280,10 +205,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -138.04168562826547, - -35.12478156027714 - ] + "coordinates": [-138.04168562826547, -35.12478156027714] }, "properties": {} }, @@ -291,10 +213,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -7.345168851766726, - 10.815197462658052 - ] + "coordinates": [-7.345168851766726, 10.815197462658052] }, "properties": {} }, @@ -302,10 +221,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -88.88438521329691, - -51.135607358754754 - ] + "coordinates": [-88.88438521329691, -51.135607358754754] }, "properties": {} }, @@ -313,10 +229,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 138.43728672485503, - -49.28473394997033 - ] + "coordinates": [138.43728672485503, -49.28473394997033] }, "properties": {} }, @@ -324,10 +237,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 121.91193801130521, - 43.62048233138311 - ] + "coordinates": [121.91193801130521, 43.62048233138311] }, "properties": {} }, @@ -335,10 +245,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -106.01217795615815, - 86.58776759386622 - ] + "coordinates": [-106.01217795615815, 86.58776759386622] }, "properties": {} }, @@ -346,10 +253,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 155.15099140482403, - 64.4291884202601 - ] + "coordinates": [155.15099140482403, 64.4291884202601] }, "properties": {} }, @@ -357,10 +261,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 6.285539827907183, - 43.07090989642467 - ] + "coordinates": [6.285539827907183, 43.07090989642467] }, "properties": {} }, @@ -368,10 +269,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 173.4289638276471, - 82.07788534436796 - ] + "coordinates": [173.4289638276471, 82.07788534436796] }, "properties": {} }, @@ -379,10 +277,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -135.69217883585586, - -85.81180716608387 - ] + "coordinates": [-135.69217883585586, -85.81180716608387] }, "properties": {} }, @@ -390,10 +285,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -60.29750446366873, - 63.14262347620537 - ] + "coordinates": [-60.29750446366873, 63.14262347620537] }, "properties": {} }, @@ -401,10 +293,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -62.65524929885156, - 82.76057831274042 - ] + "coordinates": [-62.65524929885156, 82.76057831274042] }, "properties": {} }, @@ -412,10 +301,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 3.647638695141726, - 60.28437784958815 - ] + "coordinates": [3.647638695141726, 60.28437784958815] }, "properties": {} }, @@ -423,10 +309,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 66.07623950766029, - -53.519719711469676 - ] + "coordinates": [66.07623950766029, -53.519719711469676] }, "properties": {} }, @@ -434,10 +317,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -103.32561477332257, - -28.078600679457146 - ] + "coordinates": [-103.32561477332257, -28.078600679457146] }, "properties": {} }, @@ -445,10 +325,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 117.02433904022372, - -6.075675781895833 - ] + "coordinates": [117.02433904022372, -6.075675781895833] }, "properties": {} }, @@ -456,10 +333,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -77.05424621483276, - -88.18362440176043 - ] + "coordinates": [-77.05424621483276, -88.18362440176043] }, "properties": {} }, @@ -467,10 +341,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -107.24413765555809, - -53.31111388966479 - ] + "coordinates": [-107.24413765555809, -53.31111388966479] }, "properties": {} }, @@ -478,10 +349,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -46.29469818008551, - 77.06906016616188 - ] + "coordinates": [-46.29469818008551, 77.06906016616188] }, "properties": {} }, @@ -489,10 +357,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 90.62559625004982, - -58.12741750202258 - ] + "coordinates": [90.62559625004982, -58.12741750202258] }, "properties": {} }, @@ -500,10 +365,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 72.97403970358879, - 53.71220806128216 - ] + "coordinates": [72.97403970358879, 53.71220806128216] }, "properties": {} }, @@ -511,10 +373,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -6.423696645118504, - -54.62053611672645 - ] + "coordinates": [-6.423696645118504, -54.62053611672645] }, "properties": {} }, @@ -522,10 +381,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -92.57863432629472, - 39.1899433173881 - ] + "coordinates": [-92.57863432629472, 39.1899433173881] }, "properties": {} }, @@ -533,10 +389,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 94.3018895274722, - -37.87978124619209 - ] + "coordinates": [94.3018895274722, -37.87978124619209] }, "properties": {} }, @@ -544,10 +397,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -59.63452453725998, - 9.312835101197606 - ] + "coordinates": [-59.63452453725998, 9.312835101197606] }, "properties": {} }, @@ -555,10 +405,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 32.74313358469327, - -58.71018187460506 - ] + "coordinates": [32.74313358469327, -58.71018187460506] }, "properties": {} }, @@ -566,10 +413,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 54.08213637169812, - 73.56937517387249 - ] + "coordinates": [54.08213637169812, 73.56937517387249] }, "properties": {} }, @@ -577,10 +421,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 71.66115695922338, - 89.34765770532591 - ] + "coordinates": [71.66115695922338, 89.34765770532591] }, "properties": {} }, @@ -588,10 +429,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 148.22009937003904, - 16.440042348147017 - ] + "coordinates": [148.22009937003904, 16.440042348147017] }, "properties": {} }, @@ -599,10 +437,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 109.96961209627963, - 1.317883999627374 - ] + "coordinates": [109.96961209627963, 1.317883999627374] }, "properties": {} }, @@ -610,10 +445,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 89.11392432194302, - 7.187090763173218 - ] + "coordinates": [89.11392432194302, 7.187090763173218] }, "properties": {} }, @@ -621,10 +453,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 7.397089922527362, - -14.49479933018607 - ] + "coordinates": [7.397089922527362, -14.49479933018607] }, "properties": {} }, @@ -632,10 +461,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -63.33632164620309, - 6.614277893452294 - ] + "coordinates": [-63.33632164620309, 6.614277893452294] }, "properties": {} }, @@ -643,10 +469,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 55.60109788024797, - -50.53410339369433 - ] + "coordinates": [55.60109788024797, -50.53410339369433] }, "properties": {} }, @@ -654,10 +477,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 32.18277610111382, - 22.619992552016637 - ] + "coordinates": [32.18277610111382, 22.619992552016637] }, "properties": {} }, @@ -665,10 +485,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 117.20235858298702, - -38.274881672283044 - ] + "coordinates": [117.20235858298702, -38.274881672283044] }, "properties": {} }, @@ -676,10 +493,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -148.48865535761198, - -45.39820754754232 - ] + "coordinates": [-148.48865535761198, -45.39820754754232] }, "properties": {} }, @@ -687,10 +501,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -172.18848840185686, - -4.842754863901573 - ] + "coordinates": [-172.18848840185686, -4.842754863901573] }, "properties": {} }, @@ -698,10 +509,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 43.00795052700316, - -44.82394719401794 - ] + "coordinates": [43.00795052700316, -44.82394719401794] }, "properties": {} }, @@ -709,10 +517,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 7.984586790965764, - -76.02586619776837 - ] + "coordinates": [7.984586790965764, -76.02586619776837] }, "properties": {} }, @@ -720,10 +525,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 68.39612181028589, - 43.278002948463346 - ] + "coordinates": [68.39612181028589, 43.278002948463346] }, "properties": {} }, @@ -731,10 +533,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 132.2033282476544, - 27.382666929097876 - ] + "coordinates": [132.2033282476544, 27.382666929097876] }, "properties": {} }, @@ -742,10 +541,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -162.00643498458444, - 36.01867885338586 - ] + "coordinates": [-162.00643498458444, 36.01867885338586] }, "properties": {} }, @@ -753,10 +549,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 43.346305680126285, - 51.45674000464121 - ] + "coordinates": [43.346305680126285, 51.45674000464121] }, "properties": {} }, @@ -764,10 +557,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 60.99605027146011, - -75.43512302422783 - ] + "coordinates": [60.99605027146011, -75.43512302422783] }, "properties": {} }, @@ -775,10 +565,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 117.286795694323, - -33.18659469474927 - ] + "coordinates": [117.286795694323, -33.18659469474927] }, "properties": {} }, @@ -786,10 +573,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -104.77795396770827, - -65.654248002992 - ] + "coordinates": [-104.77795396770827, -65.654248002992] }, "properties": {} }, @@ -797,10 +581,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -83.81159332418417, - 89.66881367669653 - ] + "coordinates": [-83.81159332418417, 89.66881367669653] }, "properties": {} }, @@ -808,10 +589,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -145.1702655049707, - -63.04204514510235 - ] + "coordinates": [-145.1702655049707, -63.04204514510235] }, "properties": {} }, @@ -819,10 +597,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 51.73970902606021, - 72.56187337601145 - ] + "coordinates": [51.73970902606021, 72.56187337601145] }, "properties": {} }, @@ -830,10 +605,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -89.51497017142346, - -11.642594253819322 - ] + "coordinates": [-89.51497017142346, -11.642594253819322] }, "properties": {} }, @@ -841,10 +613,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 78.90593679381274, - -37.74321668577914 - ] + "coordinates": [78.90593679381274, -37.74321668577914] }, "properties": {} }, @@ -852,10 +621,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -151.65592145690476, - 83.91736494909414 - ] + "coordinates": [-151.65592145690476, 83.91736494909414] }, "properties": {} }, @@ -863,10 +629,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 101.09924453183163, - -86.4968375258272 - ] + "coordinates": [101.09924453183163, -86.4968375258272] }, "properties": {} }, @@ -874,10 +637,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 89.5887631550857, - -70.98316756584224 - ] + "coordinates": [89.5887631550857, -70.98316756584224] }, "properties": {} }, @@ -885,10 +645,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 110.88640089233499, - -61.394756050132145 - ] + "coordinates": [110.88640089233499, -61.394756050132145] }, "properties": {} }, @@ -896,10 +653,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -6.408007048114506, - -13.206655474747105 - ] + "coordinates": [-6.408007048114506, -13.206655474747105] }, "properties": {} }, @@ -907,10 +661,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -80.34145911471012, - 47.09413813141 - ] + "coordinates": [-80.34145911471012, 47.09413813141] }, "properties": {} }, @@ -918,10 +669,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -98.52823413653272, - -75.34243719432504 - ] + "coordinates": [-98.52823413653272, -75.34243719432504] }, "properties": {} }, @@ -929,10 +677,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -8.397659944248783, - -41.335025921318504 - ] + "coordinates": [-8.397659944248783, -41.335025921318504] }, "properties": {} }, @@ -940,10 +685,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 156.84815091050078, - 25.87816836338686 - ] + "coordinates": [156.84815091050078, 25.87816836338686] }, "properties": {} }, @@ -951,10 +693,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 139.09672720530364, - 37.35325706292393 - ] + "coordinates": [139.09672720530364, 37.35325706292393] }, "properties": {} }, @@ -962,10 +701,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 8.793646682558673, - -34.68886988597508 - ] + "coordinates": [8.793646682558673, -34.68886988597508] }, "properties": {} }, @@ -973,10 +709,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 106.93267903996136, - -20.689254088169832 - ] + "coordinates": [106.93267903996136, -20.689254088169832] }, "properties": {} }, @@ -984,10 +717,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 76.51651800557826, - 66.15715833700494 - ] + "coordinates": [76.51651800557826, 66.15715833700494] }, "properties": {} }, @@ -995,10 +725,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -179.33101817473622, - -24.97493845676012 - ] + "coordinates": [-179.33101817473622, -24.97493845676012] }, "properties": {} }, @@ -1006,10 +733,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 43.88474876000892, - 60.44898938781364 - ] + "coordinates": [43.88474876000892, 60.44898938781364] }, "properties": {} }, @@ -1017,10 +741,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - -23.74702773345115, - 56.44480168552528 - ] + "coordinates": [-23.74702773345115, 56.44480168552528] }, "properties": {} }, @@ -1028,10 +749,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 89.1965818771476, - -63.127928972824996 - ] + "coordinates": [89.1965818771476, -63.127928972824996] }, "properties": {} }, @@ -1039,10 +757,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 139.17803502371603, - 76.80553072643481 - ] + "coordinates": [139.17803502371603, 76.80553072643481] }, "properties": {} }, @@ -1050,10 +765,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 138.04162199308098, - -14.6234595277084 - ] + "coordinates": [138.04162199308098, -14.6234595277084] }, "properties": {} }, @@ -1061,10 +773,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 174.31317825590452, - 12.57531175694111 - ] + "coordinates": [174.31317825590452, 12.57531175694111] }, "properties": {} }, @@ -1072,10 +781,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 36.826555455819346, - 15.871073075400028 - ] + "coordinates": [36.826555455819346, 15.871073075400028] }, "properties": {} }, @@ -1083,10 +789,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 84.65234855310884, - 15.736922349868095 - ] + "coordinates": [84.65234855310884, 15.736922349868095] }, "properties": {} }, @@ -1094,10 +797,7 @@ "type": "Feature", "geometry": { "type": "Point", - "coordinates": [ - 97.5413389416346, - -58.1831539942764 - ] + "coordinates": [97.5413389416346, -58.1831539942764] }, "properties": {} } diff --git a/web/.storybook/public/testdata/sample.gltf b/web/.storybook/public/testdata/sample.gltf index e7d915e191..5e7945f846 100644 --- a/web/.storybook/public/testdata/sample.gltf +++ b/web/.storybook/public/testdata/sample.gltf @@ -1,70 +1,72 @@ - { +{ "scene": 0, - "scenes" : [ + "scenes": [ { - "nodes" : [ 0 ] + "nodes": [0] } ], - - "nodes" : [ + + "nodes": [ { - "mesh" : 0 + "mesh": 0 } ], - - "meshes" : [ + + "meshes": [ { - "primitives" : [ { - "attributes" : { - "POSITION" : 1 - }, - "indices" : 0 - } ] + "primitives": [ + { + "attributes": { + "POSITION": 1 + }, + "indices": 0 + } + ] } ], - "buffers" : [ + "buffers": [ { - "uri" : "data:application/octet-stream;base64,AAABAAIAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAA=", - "byteLength" : 44 + "uri": "data:application/octet-stream;base64,AAABAAIAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAA=", + "byteLength": 44 } ], - "bufferViews" : [ + "bufferViews": [ { - "buffer" : 0, - "byteOffset" : 0, - "byteLength" : 6, - "target" : 34963 + "buffer": 0, + "byteOffset": 0, + "byteLength": 6, + "target": 34963 }, { - "buffer" : 0, - "byteOffset" : 8, - "byteLength" : 36, - "target" : 34962 + "buffer": 0, + "byteOffset": 8, + "byteLength": 36, + "target": 34962 } ], - "accessors" : [ + "accessors": [ { - "bufferView" : 0, - "byteOffset" : 0, - "componentType" : 5123, - "count" : 3, - "type" : "SCALAR", - "max" : [ 2 ], - "min" : [ 0 ] + "bufferView": 0, + "byteOffset": 0, + "componentType": 5123, + "count": 3, + "type": "SCALAR", + "max": [2], + "min": [0] }, { - "bufferView" : 1, - "byteOffset" : 0, - "componentType" : 5126, - "count" : 3, - "type" : "VEC3", - "max" : [ 1.0, 1.0, 0.0 ], - "min" : [ 0.0, 0.0, 0.0 ] + "bufferView": 1, + "byteOffset": 0, + "componentType": 5126, + "count": 3, + "type": "VEC3", + "max": [1.0, 1.0, 0.0], + "min": [0.0, 0.0, 0.0] } ], - - "asset" : { - "version" : "2.0" + + "asset": { + "version": "2.0" } } diff --git a/web/.storybook/public/tileset/tileset.json b/web/.storybook/public/tileset/tileset.json index eaddb399a2..e18258af72 100644 --- a/web/.storybook/public/tileset/tileset.json +++ b/web/.storybook/public/tileset/tileset.json @@ -5,38 +5,13 @@ "geometricError": 500, "root": { "transform": [ - 96.86356343768793, - 24.848542777253734, - 0, - 0, - -15.986465724980844, - 62.317780594908875, - 76.5566922962899, - 0, - 19.02322243409411, - -74.15554020821229, - 64.3356267137516, - 0, - 1215107.7612304366, - -4736682.902037748, - 4081926.095098698, - 1 + 96.86356343768793, 24.848542777253734, 0, 0, -15.986465724980844, + 62.317780594908875, 76.5566922962899, 0, 19.02322243409411, + -74.15554020821229, 64.3356267137516, 0, 1215107.7612304366, + -4736682.902037748, 4081926.095098698, 1 ], "boundingVolume": { - "box": [ - 0, - 0, - 0, - 7.0955, - 0, - 0, - 0, - 3.1405, - 0, - 0, - 0, - 5.0375 - ] + "box": [0, 0, 0, 7.0955, 0, 0, 0, 3.1405, 0, 0, 0, 5.0375] }, "geometricError": 1, "refine": "REPLACE", @@ -46,20 +21,7 @@ "children": [ { "boundingVolume": { - "box": [ - 0, - 0, - 0, - 7.0955, - 0, - 0, - 0, - 3.1405, - 0, - 0, - 0, - 5.0375 - ] + "box": [0, 0, 0, 7.0955, 0, 0, 0, 3.1405, 0, 0, 0, 5.0375] }, "geometricError": 0.1, "content": { @@ -68,20 +30,7 @@ "children": [ { "boundingVolume": { - "box": [ - 0, - 0, - 0, - 7.0955, - 0, - 0, - 0, - 3.1405, - 0, - 0, - 0, - 5.0375 - ] + "box": [0, 0, 0, 7.0955, 0, 0, 0, 3.1405, 0, 0, 0, 5.0375] }, "geometricError": 0, "content": { diff --git a/web/.storybook/theme.ts b/web/.storybook/theme.ts index 2a97be1046..8e773e11be 100644 --- a/web/.storybook/theme.ts +++ b/web/.storybook/theme.ts @@ -9,5 +9,5 @@ export default create({ brandImage: "/logo.svg", brandTarget: "_self", colorPrimary: "#E95518", - colorSecondary: "#4770FF", + colorSecondary: "#4770FF" }); diff --git a/web/.vscode/settings.json b/web/.vscode/settings.json index 170b9e5d8e..7dee390f26 100644 --- a/web/.vscode/settings.json +++ b/web/.vscode/settings.json @@ -1,7 +1,7 @@ { - "typescript.tsdk": "node_modules/typescript/lib", - "yaml.schemas": { - "https://json.schemastore.org/github-workflow": "/.github/workflows/**/*.yml" - }, - "vitest.commandLine": "yarn run vitest watch" + "typescript.tsdk": "node_modules/typescript/lib", + "yaml.schemas": { + "https://json.schemastore.org/github-workflow": "/.github/workflows/**/*.yml" + }, + "vitest.commandLine": "yarn run vitest watch" } diff --git a/web/CODE_OF_CONDUCT.md b/web/CODE_OF_CONDUCT.md index ae596ec118..2969f5f5a4 100644 --- a/web/CODE_OF_CONDUCT.md +++ b/web/CODE_OF_CONDUCT.md @@ -17,23 +17,23 @@ diverse, inclusive, and healthy community. Examples of behavior that contributes to a positive environment for our community include: -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience -* Focusing on what is best not just for us as individuals, but for the +- Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: -* The use of sexualized language or imagery, and sexual attention or +- The use of sexualized language or imagery, and sexual attention or advances of any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a +- Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities @@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an +standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within @@ -118,11 +118,11 @@ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. -Community Impact Guidelines were inspired by +Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. For answers to common questions about this code of conduct, see the FAQ at -[https://www.contributor-covenant.org/faq][FAQ]. Translations are available +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. [homepage]: https://www.contributor-covenant.org diff --git a/web/README.md b/web/README.md index 5628083d1a..385c7f8843 100644 --- a/web/README.md +++ b/web/README.md @@ -1,4 +1,22 @@ -[![ci-web](https://github.com/reearth/reearth-visualizer/actions/workflows/ci_web.yml/badge.svg)](https://github.com/reearth/reearth-visualizer/actions/workflows/ci_web.yml) [![codecov](https://codecov.io/gh/reearth/reearth/branch/main/graph/badge.svg?flag=web-beta,web-utils,web-classic)](https://codecov.io/gh/reearth/reearth) - # reearth/web -Documentation (coming soon) + +Web interface for Re:Earth GIS software built with React 18 and Vite. + +## Getting started + +### Development + +Add environment variables to .env. A template of the required variables are in `env.local`. +Consult the development team for the correct environment variables. + +`yarn start` to start the local server. The interface will open on [http://localhost:3000](http://localhost:3000) + +`yarn build` TBC + +`yarn format` to lint code + +`yarn storybook` to open Storybook for web interface's UI on [http://localhost:9001](http://localhost:9001) + +`yarn gql` automatically generates typing of GraphQL operations. + +`yarn i18n` parses components to provide internationalisation for texts in supported languages. diff --git a/web/bin/pluginDoc.ts b/web/bin/pluginDoc.ts index cb82202904..4cc1d813d7 100644 --- a/web/bin/pluginDoc.ts +++ b/web/bin/pluginDoc.ts @@ -23,13 +23,13 @@ type P = { for (const s of program.getSourceFiles()) { if (s.fileName.substr(-5) === ".d.ts") continue; const decls: D[] = []; - ts.forEachChild(s, node => { + ts.forEachChild(s, (node) => { const d = parse(node); if (d) { decls.push(d); } }); - const res = render(decls, n => decls.findIndex(d => d.name === n) != -1); + const res = render(decls, (n) => decls.findIndex((d) => d.name === n) != -1); // writeFileSync("./docs/plugin.md", res); console.log(res); } @@ -50,7 +50,7 @@ function parse(node: ts.Node): D | undefined { name: s.name, type: tc.typeToString(t), required: (s.flags & ts.SymbolFlags.Optional) === 0, - desc: ts.displayPartsToString(s.getDocumentationComment(tc)), + desc: ts.displayPartsToString(s.getDocumentationComment(tc)) }; }) .filter((p): p is P => !!p); @@ -58,17 +58,19 @@ function parse(node: ts.Node): D | undefined { return { name: node.name.text, desc: ts.displayPartsToString(sym.getDocumentationComment(tc)), - properties, + properties }; } function render(decls: D[], linkable: (name: string) => boolean): string { return decls - .flatMap(d => [ + .flatMap((d) => [ `# ${d.name}`, d.desc, - d.properties.map(p => `- ${renderHead(p, linkable)}`).join("\n"), - ...d.properties.filter(p => p.desc).flatMap(p => [`## ${renderHead(p, linkable)}`, p.desc]), + d.properties.map((p) => `- ${renderHead(p, linkable)}`).join("\n"), + ...d.properties + .filter((p) => p.desc) + .flatMap((p) => [`## ${renderHead(p, linkable)}`, p.desc]) ]) .filter(Boolean) .join("\n\n"); @@ -78,17 +80,37 @@ function renderHead(p: P, linkable: (name: string) => boolean): string { return [p.name, ": ", renderType(p.type, p.required, linkable)].join(""); } -function renderType(type: string, required: boolean, linkable: (name: string) => boolean) { - const ts = split(type, [") => ", "[]", "?: ", ": ", " & ", " | ", "(", ")", ", "]) - .map(s => +function renderType( + type: string, + required: boolean, + linkable: (name: string) => boolean +) { + const ts = split(type, [ + ") => ", + "[]", + "?: ", + ": ", + " & ", + " | ", + "(", + ")", + ", " + ]) + .map((s) => typeof s === "string" && linkable(s) ? { - link: s, + link: s } - : s, + : s ) - .map(s => - typeof s === "string" ? s : "link" in s ? `[${s.link}](#${s.link})` : "s" in s ? s.s : "", + .map((s) => + typeof s === "string" + ? s + : "link" in s + ? `[${s.link}](#${s.link})` + : "s" in s + ? s.s + : "" ) .join(""); const func = / => /.test(type); @@ -96,7 +118,7 @@ function renderType(type: string, required: boolean, linkable: (name: string) => !required && func ? "(" : "", ts, !required && func ? ")" : "", - !required ? " | undefined" : "", + !required ? " | undefined" : "" ].join(""); } @@ -104,9 +126,9 @@ function split(text: string, splitter: string[]): (string | { s: string })[] { if (!text.length) return []; const res: (string | { s: string })[] = []; let buf = ""; - for (let i = 0; i < text.length; i++) { - buf += text[i]; - const s = splitter.find(s => buf.includes(s)); + for (const t of text) { + buf += t; + const s = splitter.find((s) => buf.includes(s)); if (s) { res.push(buf.slice(0, -s.length), { s: buf.slice(-s.length) }); buf = ""; diff --git a/web/codegen.ts b/web/codegen.ts index a9c2e23588..9315424f21 100644 --- a/web/codegen.ts +++ b/web/codegen.ts @@ -7,13 +7,16 @@ const rootGenerateDirectory = `${rootGQLDirectory}__gen__/`; const config: CodegenConfig = { overwrite: true, schema: "../server/gql/*.graphql", - documents: [`${rootGQLDirectory}fragments/*.ts`, `${rootGQLDirectory}queries/*.ts`], + documents: [ + `${rootGQLDirectory}fragments/*.ts`, + `${rootGQLDirectory}queries/*.ts` + ], generates: { [rootGenerateDirectory]: { preset: "client", presetConfig: { gqlTagName: "gql", - fragmentMasking: false, + fragmentMasking: false }, config: { useTypeImports: true, @@ -25,15 +28,15 @@ const config: CodegenConfig = { URL: "string", Lang: "string", TranslatedString: "{ [lang in string]?: string } | null", - JSON: "any", - }, - }, + JSON: "any" + } + } }, [`${rootGenerateDirectory}/fragmentMatcher.json`]: { - plugins: ["fragment-matcher"], - }, + plugins: ["fragment-matcher"] + } }, - ignoreNoDocuments: true, + ignoreNoDocuments: true }; export default config; diff --git a/web/e2e/utils/config.ts b/web/e2e/utils/config.ts index 8734db8b86..1ad5d2cb43 100644 --- a/web/e2e/utils/config.ts +++ b/web/e2e/utils/config.ts @@ -9,8 +9,7 @@ export const config = { authUrl: process.env["REEARTH_WEB_AUTH0_DOMAIN"], signUpSecret: process.env["REEARTH_WEB_E2E_SIGNUP_SECRET"], authProvider: process.env["REEARTH_WEB_AUTH_PROVIDER"], - developerMode: process.env["REEARTH_WEB_DEVELOPER_MODE"], - earlyAccessAdmins: process.env["REEARTH_WEB_EARLY_ACCESS_ADMINS"], + earlyAccessAdmins: process.env["REEARTH_WEB_EARLY_ACCESS_ADMINS"] }; export type Config = typeof config; diff --git a/web/e2e/utils/index.ts b/web/e2e/utils/index.ts index 42cfb8acd3..56548dc917 100644 --- a/web/e2e/utils/index.ts +++ b/web/e2e/utils/index.ts @@ -1,5 +1,10 @@ // eslint-disable-next-line no-restricted-imports -import { type APIRequestContext, request, test as base, type Page } from "@playwright/test"; +import { + type APIRequestContext, + request, + test as base, + type Page +} from "@playwright/test"; import { config, getAccessToken, type Config } from "./config"; @@ -18,7 +23,7 @@ export type Reearth = { gql: ( query: string, variables?: Record, - options?: { ignoreError?: boolean }, + options?: { ignoreError?: boolean } ) => Promise; } & Config; @@ -35,7 +40,9 @@ export const test = base.extend<{ async goto(url, options) { const res = await page.goto(url, options); if (this.token) { - await page.evaluate(`window.REEARTH_E2E_ACCESS_TOKEN = ${JSON.stringify(this.token)};`); + await page.evaluate( + `window.REEARTH_E2E_ACCESS_TOKEN = ${JSON.stringify(this.token)};` + ); } return res; }, @@ -45,11 +52,11 @@ export const test = base.extend<{ const resp = await request.post(config.api + "/graphql", { data: { query, - variables, + variables }, headers: { - Authorization: `Bearer ${this.token}`, - }, + Authorization: `Bearer ${this.token}` + } }); const body = await resp.json(); @@ -58,14 +65,14 @@ export const test = base.extend<{ } return body; - }, + } }); - }, + } }); export async function initUser( token: string, - ctx?: APIRequestContext, + ctx?: APIRequestContext ): Promise<{ token: string; userId: string; @@ -85,8 +92,8 @@ export async function initUser( userId, teamId, api, - signUpSecret: signUpSecret ? "***" : "", - })}`, + signUpSecret: signUpSecret ? "***" : "" + })}` ); } @@ -102,12 +109,12 @@ export async function initUser( userId, teamId, secret: signUpSecret, - lang: "en", - }, + lang: "en" + } }, headers: { - Authorization: `Bearer ${token}`, - }, + Authorization: `Bearer ${token}` + } }); const body = await resp.json(); @@ -118,8 +125,8 @@ export async function initUser( userId, teamId, api, - signUpSecret: signUpSecret ? "***" : "", - })}`, + signUpSecret: signUpSecret ? "***" : "" + })}` ); } @@ -127,6 +134,6 @@ export async function initUser( token, userName, userId: body.data.signup.user.id, - teamId, + teamId }; } diff --git a/web/e2e/utils/login.ts b/web/e2e/utils/login.ts index 1251d46cd2..af596490e2 100644 --- a/web/e2e/utils/login.ts +++ b/web/e2e/utils/login.ts @@ -13,9 +13,9 @@ export async function login(): Promise { userName, password: password ? "***" : "", authAudience, - authClientId, - }, - )}`, + authClientId + } + )}` ); } @@ -28,8 +28,8 @@ export async function login(): Promise { audience: authAudience, client_id: authClientId, grant_type: "password", - scope: "openid profile email", - }, + scope: "openid profile email" + } ); if (!resp.data.access_token) { @@ -43,8 +43,8 @@ export async function login(): Promise { userName, password: password ? "***" : "", authAudience, - authClientId, - })}`, + authClientId + })}` ); } } diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs new file mode 100644 index 0000000000..ddb38d5236 --- /dev/null +++ b/web/eslint.config.mjs @@ -0,0 +1,40 @@ +import config from "eslint-config-reearth"; +import playwright from "eslint-plugin-playwright"; + +/** @type { import("eslint").Linter.Config[] } */ + +const themeConfig = [ + { + rules: { + "@typescript-eslint/consistent-type-definitions": "off", + "@typescript-eslint/no-empty-object-type": [ + "error", + { + allowInterfaces: "with-single-extends" + } + ] + } + } +]; + +const e2eConfig = [ + { + files: ["e2e/**/*"], + ...playwright.configs["flat/recommended"], + rules: { + "no-restricted-imports": [ + "error", + { + paths: [ + { + name: "@playwright/test", + message: "Use @reearth/e2e/utils instead." + } + ] + } + ] + } + } +]; + +export default [...config("@reearth"), ...e2eConfig, ...themeConfig]; diff --git a/web/i18next-parser.config.js b/web/i18next-parser.config.js index 403bb187f6..4723a3bb53 100644 --- a/web/i18next-parser.config.js +++ b/web/i18next-parser.config.js @@ -5,5 +5,5 @@ export default { // allow keys to be phrases having `:`, `.` namespaceSeparator: false, keySeparator: false, - createOldCatalogs: false, + createOldCatalogs: false }; diff --git a/web/index.html b/web/index.html index ab4c00810b..43379d90c8 100644 --- a/web/index.html +++ b/web/index.html @@ -1,21 +1,22 @@ - + + + + Re:Earth + + + + + + - - - Re:Earth - - - - - - - - -
- - - + +
+ + - - - + +
+ + + diff --git a/web/src/app.tsx b/web/src/app.tsx index fc82cc03d8..59f7ce196b 100644 --- a/web/src/app.tsx +++ b/web/src/app.tsx @@ -1,10 +1,10 @@ -import { Suspense } from "react"; -import Loading from "@reearth/beta/components/Loading"; import GlobalModal from "@reearth/beta/features/GlobalModal"; import NotificationBanner from "@reearth/beta/features/Notification"; import { Provider as I18nProvider } from "@reearth/services/i18n"; +import { Suspense } from "react"; +import { Loading } from "./beta/lib/reearth-ui"; import { AuthProvider } from "./services/auth"; import { Provider as GqlProvider } from "./services/gql"; import { AppRoutes } from "./services/routing"; @@ -16,7 +16,9 @@ export default function App() { - }> + } + > diff --git a/web/src/beta/components/Accordion/AccordionItem.tsx b/web/src/beta/components/Accordion/AccordionItem.tsx deleted file mode 100644 index e3790456ff..0000000000 --- a/web/src/beta/components/Accordion/AccordionItem.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import React from "react"; -import { - AccordionItem as AccordionItemComponent, - AccordionItemButton, - AccordionItemHeading, - AccordionItemPanel, - AccordionItemState, -} from "react-accessible-accordion"; - -import { styled, useTheme } from "@reearth/services/theme"; - -import Icon from "../Icon"; - -export type Props = { - className?: string; - id: string; - heading?: React.ReactNode; - content?: React.ReactNode; - bg?: string; -}; - -const AccordionItem: React.FC = ({ className, id, heading, content, bg }) => { - const theme = useTheme(); - return ( - - - - - - - {({ expanded }) => ( - <> - - {heading} - - )} - - - - - - {content} - - - - ); -}; - -const Wrapper = styled.div<{ bg?: string }>` - margin: ${({ theme }) => theme.spacing.super}px 0; - background-color: ${({ bg }) => bg}; - border-radius: ${({ theme }) => theme.spacing.large}px; -`; - -const AccordionItemStateWrapper = styled.div` - display: flex; - align-items: center; - padding: ${({ theme }) => theme.spacing.largest}px; -`; - -const StyledIcon = styled(Icon)<{ open: boolean }>` - transition: transform 0.15s ease; - transform: ${({ open }) => open && "translateY(10%) rotate(90deg)"}; - margin-right: 24px; -`; - -const StyledAccordionItemButton = styled(AccordionItemButton)` - outline: none; - cursor: pointer; -`; -export default AccordionItem; diff --git a/web/src/beta/components/Accordion/index.stories.tsx b/web/src/beta/components/Accordion/index.stories.tsx deleted file mode 100644 index 7dfbbf6adb..0000000000 --- a/web/src/beta/components/Accordion/index.stories.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { Meta, StoryObj } from "@storybook/react"; - -import Accordion from "."; - -const meta: Meta = { - component: Accordion, -}; - -export default meta; - -type Story = StoryObj; - -const SampleHeading =
heading
; - -const SampleContent = ( -
hoge
-); - -export const Default: Story = { - render: () => ( - - ), -}; diff --git a/web/src/beta/components/Accordion/index.test.tsx b/web/src/beta/components/Accordion/index.test.tsx deleted file mode 100644 index f3436a8f3e..0000000000 --- a/web/src/beta/components/Accordion/index.test.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { expect, test } from "vitest"; - -import { fireEvent, render, screen } from "@reearth/test/utils"; - -import Accordion, { AccordionItemType } from "./index"; - -const sampleContents: AccordionItemType[] = [ - { - id: "1", - heading:
This is heading1
, - content:
This is content1
, - }, - { - id: "2", - heading:
This is heading2
, - content:
This is content2
, - }, -]; - -test("should be rendered", () => { - render(); -}); - -test("should display items header", () => { - render(); - expect(screen.getByTestId("atoms-accordion")).toBeInTheDocument(); - expect(screen.getByText(/heading1/)).toBeInTheDocument(); - expect(screen.getByText(/heading2/)).toBeInTheDocument(); -}); - -test("should display items content", () => { - render(); - expect(screen.getByText(/content1/)).toBeInTheDocument(); - expect(screen.getByText(/content2/)).toBeInTheDocument(); -}); - -test("should open when header button is clicked", () => { - render(); - expect(screen.getAllByTestId("atoms-accordion-item-content")[0]).not.toBeVisible(); - fireEvent.click(screen.getAllByTestId("atoms-accordion-item-header")[0]); - expect(screen.getAllByTestId("atoms-accordion-item-content")[0]).toBeVisible(); -}); diff --git a/web/src/beta/components/Accordion/index.tsx b/web/src/beta/components/Accordion/index.tsx deleted file mode 100644 index 0595e2787f..0000000000 --- a/web/src/beta/components/Accordion/index.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { Accordion as AccordionComponent } from "react-accessible-accordion"; - -import AccordionItem from "./AccordionItem"; - -export type Props = { - className?: string; - items?: AccordionItemType[]; - allowZeroExpanded?: boolean; - allowMultipleExpanded?: boolean; - itemBgColor?: string; -}; - -export type AccordionItemType = { - id: string; - heading?: React.ReactNode; - content?: React.ReactNode; -}; - -const Accordion: React.FC = ({ - className, - items, - allowMultipleExpanded, - allowZeroExpanded = true, - itemBgColor, -}) => { - return ( - - {items?.map(i => { - return ( - - ); - })} - - ); -}; - -export default Accordion; diff --git a/web/src/beta/components/Avatar/index.stories.tsx b/web/src/beta/components/Avatar/index.stories.tsx deleted file mode 100644 index dfa48d7f2d..0000000000 --- a/web/src/beta/components/Avatar/index.stories.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Meta, StoryObj } from "@storybook/react"; - -import Avatar from "."; - -const meta: Meta = { - component: Avatar, -}; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = { render: () => }; -export const Small: Story = { render: () => }; -export const Large: Story = { render: () => }; diff --git a/web/src/beta/components/Avatar/index.tsx b/web/src/beta/components/Avatar/index.tsx deleted file mode 100644 index 162a9f9a14..0000000000 --- a/web/src/beta/components/Avatar/index.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React from "react"; - -import Flex from "@reearth/beta/components/Flex"; -import Text from "@reearth/beta/components/Text"; -import { styled, useTheme } from "@reearth/services/theme"; - -export type Size = "small" | "large"; - -export type Props = { - className?: string; - size?: Size; - background?: string; - avatar?: string; - innerText?: string | number; - borderRadius?: string; -}; - -const Avatar: React.FC = ({ - className, - size = "small", - background, - avatar, - innerText, - borderRadius = "50%", -}) => { - const theme = useTheme(); - - return ( - - {innerText && ( - - {typeof innerText === "number" ? `+${innerText.toString()}` : innerText.charAt(0)} - - )} - - ); -}; -const StyledAvatar = styled(Flex)<{ - size?: Size; - avatar?: string; - background?: string; - borderRadius?: string; -}>` - box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); - width: ${({ size }) => (size === "large" ? "64px" : "32px")}; - height: ${({ size }) => (size === "large" ? "64px" : "32px")}; - border-radius: ${({ borderRadius }) => borderRadius ?? "50%"}; - background: ${({ avatar, background }) => (avatar ? `url(${avatar});` : background)}; -`; - -export default Avatar; diff --git a/web/src/beta/components/Box/index.stories.tsx b/web/src/beta/components/Box/index.stories.tsx deleted file mode 100644 index 4ae39771e5..0000000000 --- a/web/src/beta/components/Box/index.stories.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Meta, Story } from "@storybook/react"; - -import Component, { Props } from "."; - -export default { - title: "beta/components/Box", - component: Component, -} as Meta; - -export const Margin: Story = () => ( - -
Margin
-
-); -export const Padding: Story = () => ( - -
Padding
-
-); -export const Border: Story = () => ( - -
Border
-
-); -export const Bg: Story = () => ( - -
Bg
-
-); diff --git a/web/src/beta/components/Box/index.tsx b/web/src/beta/components/Box/index.tsx deleted file mode 100644 index 674614eeff..0000000000 --- a/web/src/beta/components/Box/index.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { CSSProperties } from "react"; - -import { MetricsSizes, metricsSizes } from "@reearth/beta/utils/metrics"; -import { styled } from "@reearth/services/theme"; - -export type Props = { - className?: string; - children: React.ReactNode; - onClick?: () => void; -} & BoxProps; - -type Metrics = { - m?: T; - mt?: T; - mb?: T; - mr?: T; - ml?: T; - mv?: T; - mh?: T; - p?: T; - pt?: T; - pb?: T; - pr?: T; - pl?: T; - pv?: T; - ph?: T; -}; - -type MetricsProps = Metrics; - -type BorderProps = { - border?: string; - borderRadius?: number; - borderWidth?: number; - borderColor?: number; - borderStyle?: CSSProperties["borderStyle"]; -}; - -type OtherCSSProperties = { - bg?: string; - width?: CSSProperties["width"]; -}; - -type BoxProps = MetricsProps & BorderProps & OtherCSSProperties; - -const Box: React.FC = props => { - const { className, children, onClick, ...styleProps } = props; - return ( - - {children} - - ); -}; - -const Wrapper = styled.div` - background-color: ${props => props.bg}; - width: ${props => props.width}; - margin: ${props => (props.m && props.m in metricsSizes ? `${metricsSizes[props.m]}px}` : "")}; - margin: ${props => (props.mv && props.mv in metricsSizes ? `${metricsSizes[props.mv]}px 0` : "")}; - margin: ${props => (props.mh && props.mh in metricsSizes ? `0 ${metricsSizes[props.mh]}px` : "")}; - margin-top: ${props => - props.mt && props.mt in metricsSizes ? `${metricsSizes[props.mt]}px` : ""}; - margin-bottom: ${props => - props.mb && props.mb in metricsSizes ? `${metricsSizes[props.mb]}px` : ""}; - margin-right: ${props => - props.mr && props.mr in metricsSizes ? `${metricsSizes[props.mr]}px` : ""}; - margin-left: ${props => - props.ml && props.ml in metricsSizes ? `${metricsSizes[props.ml]}px` : ""}; - padding: ${props => (props.p && props.p in metricsSizes ? `${metricsSizes[props.p]}px` : "")}; - padding: ${props => - props.pv && props.pv in metricsSizes ? `${metricsSizes[props.pv]}px 0` : ""}; - padding: ${props => - props.ph && props.ph in metricsSizes ? `0 ${metricsSizes[props.ph]}px` : ""}; - padding-top: ${props => - props.pt && props.pt in metricsSizes ? `${metricsSizes[props.pt]}px` : ""}; - padding-bottom: ${props => - props.pb && props.pb in metricsSizes ? `${metricsSizes[props.pb]}px` : ""}; - padding-right: ${props => - props.pr && props.pr in metricsSizes ? `${metricsSizes[props.pr]}px` : ""}; - padding-left: ${props => - props.pl && props.pl in metricsSizes ? `${metricsSizes[props.pl]}px` : ""}; - border: ${props => props.border}; - border-radius: ${props => (props.border ? `${props.borderRadius}px` : "")}; - border-width: ${props => (props.borderWidth ? `${props.borderWidth}px` : "")}; - border-color: ${props => (props.borderColor ? `${props.borderColor}` : "")}; - border-style: ${props => (props.borderStyle ? `${props.borderStyle}` : "")}; -`; - -export default Box; diff --git a/web/src/beta/components/Button/index.stories.tsx b/web/src/beta/components/Button/index.stories.tsx index 54cb42cbd0..dcdfdc05b5 100644 --- a/web/src/beta/components/Button/index.stories.tsx +++ b/web/src/beta/components/Button/index.stories.tsx @@ -3,7 +3,7 @@ import { Meta, StoryObj } from "@storybook/react"; import Button from "."; const meta: Meta = { - component: Button, + component: Button }; export default meta; @@ -11,69 +11,89 @@ export default meta; type Story = StoryObj; export const Default: Story = { - render: () => - - {actionContent} - - )} - - ); -}; - -export default ListItem; - -const Wrapper = styled.div<{ - border?: boolean; - isSelected?: boolean; - isOpenAction?: boolean; - clamp?: Clamp; -}>` - display: flex; - min-height: 36px; - align-items: center; - color: ${({ theme }) => theme.content.main}; - border: 1px solid ${({ border, theme }) => (border ? theme.outline.weakest : "transparent")}; - border-radius: ${({ clamp }) => - clamp === "left" ? "0 4px 4px 0" : clamp === "right" ? "4px 0 0 4px" : "4px"}; - background: ${({ theme, isSelected, isOpenAction }) => - isSelected ? theme.select.main : isOpenAction ? theme.bg[3] : "inherit"}; - transition: all 0.2s; - cursor: pointer; - - :hover { - ${({ isSelected, theme }) => !isSelected && `background-color:` + theme.bg[3]} - } -`; - -const Inner = styled.div` - display: flex; - align-items: center; - width: 100%; - padding: 8px 4px 8px 4px; - border-radius: 4px; -`; - -const StyledText = styled(Text)` - flex-grow: 1; - word-break: break-all; - text-align: left; -`; - -const Button = styled.div<{ clamp?: Clamp }>` - display: flex; - align-items: center; - height: 36px; - color: ${({ theme }) => theme.content.weak}; - border-radius: ${({ clamp }) => - clamp === "left" ? "0 4px 4px 0" : clamp === "right" ? "4px 0 0 4px" : "4px"}; -`; diff --git a/web/src/beta/components/Loading/index.stories.tsx b/web/src/beta/components/Loading/index.stories.tsx deleted file mode 100644 index d4c7312b73..0000000000 --- a/web/src/beta/components/Loading/index.stories.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Meta, StoryObj } from "@storybook/react"; - -import Loading from "."; - -const meta: Meta = { - component: Loading, -}; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = { render: () => }; diff --git a/web/src/beta/components/Loading/index.tsx b/web/src/beta/components/Loading/index.tsx deleted file mode 100644 index c82a636cda..0000000000 --- a/web/src/beta/components/Loading/index.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from "react"; -import { RingLoader } from "react-spinners"; - -import { styled, useTheme } from "@reearth/services/theme"; - -import Portal from "../Portal"; - -export type Props = { - className?: string; - animationSize?: number; - animationColor?: string; - portal?: boolean; - fixed?: boolean; - relative?: boolean; - overlay?: boolean; -}; - -const Loading: React.FC = ({ - className, - animationSize, - animationColor, - portal, - fixed, - relative, - overlay, -}) => { - const theme = useTheme(); - const loading = ( - - - - ); - return portal ? {loading} : loading; -}; - -const LoadingWrapper = styled.div<{ fixed?: boolean; overlay?: boolean; relative?: boolean }>` - width: 100%; - height: 100%; - display: flex; - justify-content: center; - align-items: center; - font-size: 14px; - position: ${({ fixed, relative }) => (fixed ? "fixed" : relative ? "relative" : "absolute")}; - top: 0; - left: 0; - background: ${({ overlay, theme }) => (overlay ? theme.bg[0] : null)}; - opacity: 0.8; - z-index: ${props => props.theme.zIndexes.editor.loading}; -`; - -export default Loading; diff --git a/web/src/beta/components/Modal/ModalFrame/index.tsx b/web/src/beta/components/Modal/ModalFrame/index.tsx deleted file mode 100644 index f78f537f80..0000000000 --- a/web/src/beta/components/Modal/ModalFrame/index.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import useTransition, { TransitionStatus } from "@rot1024/use-transition"; -import { ReactNode, useRef, useCallback, useMemo } from "react"; -import { useClickAway, useKeyPressEvent } from "react-use"; - -import Icon from "@reearth/beta/components/Icon"; -import Text from "@reearth/beta/components/Text"; -import { styled } from "@reearth/services/theme"; - -export type Props = { - className?: string; - children?: ReactNode; - size?: "sm" | "md" | "lg"; - isVisible?: boolean; - title?: string; - onClose?: () => void; -}; - -const Modal: React.FC = ({ className, size, isVisible, title, onClose, children }) => { - const ref = useRef(null); - useClickAway(ref, () => onClose?.()); - - const modalWidth = useMemo( - () => (size === "sm" ? "416px" : size === "lg" ? "778px" : "572px"), - [size], - ); - - const state = useTransition(!!isVisible, 300, { - mountOnEnter: true, - unmountOnExit: true, - }); - - const handleClose = useCallback(() => { - if (isVisible) onClose?.(); - }, [onClose, isVisible]); - - useKeyPressEvent("Escape", handleClose); - - return state === "unmounted" ? null : ( - - - - {!!title && ( - - - {title} - - {onClose && } - - )} - {children} - - - - ); -}; - -const Bg = styled.div<{ state: TransitionStatus }>` - background: ${({ theme }) => theme.bg.transparentBlack}; - position: fixed; - z-index: ${({ theme }) => theme.zIndexes.editor.modal.bg}; - left: 0; - top: 0; - width: 100%; - height: 100%; - overflow: auto; - transition: ${({ state }) => - state === "entering" || state === "exiting" ? "all 0.3s ease" : ""}; - opacity: ${({ state }) => (state === "entered" || state === "entering" ? 1 : 0)}; -`; - -const CenteredWrapper = styled.div<{ width?: string }>` - margin-left: auto; - margin-right: auto; - height: 100%; - width: ${({ width }) => width}; - position: relative; - display: flex; - flex-direction: column; - justify-content: center; -`; - -const Wrapper = styled.div<{ width?: string }>` - border-radius: 8px; - background: #161616; - width: ${({ width }) => width}; -`; - -const InnerWrapper = styled.div` - margin: 0 auto; -`; - -const ModalTitle = styled(Text)` - text-align: center; - margin-right: auto; -`; - -const CloseIcon = styled(Icon)` - margin-left: auto; - cursor: pointer; -`; - -const HeaderWrapper = styled.div` - display: flex; - padding: 12px; - justify-content: space-between; - align-items: center; - align-self: stretch; - border-top-right-radius: 8px; - border-top-left-radius: 8px; - color: ${({ theme }) => theme.content.main}; - background: ${({ theme }) => theme.bg[2]}; -`; -export default Modal; diff --git a/web/src/beta/components/Modal/index.stories.tsx b/web/src/beta/components/Modal/index.stories.tsx deleted file mode 100644 index fc53f8fa90..0000000000 --- a/web/src/beta/components/Modal/index.stories.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { Meta, StoryObj } from "@storybook/react"; -import { ReactNode } from "react"; - -import Button from "../Button"; -import Text from "../Text"; - -import Modal, { SidebarTab } from "."; - -const meta: Meta = { - component: Modal, -}; - -export default meta; - -type Story = StoryObj; - -const ModalContent: React.FC = () => { - return ( - - Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of - classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a - Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin - words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in - classical literature, discovered the undoubtable source. - - ); -}; - -const TabContent: ReactNode = ( - <> - Tab Title - - Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of - classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a - Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin - words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in - classical literature, discovered the undoubtable source. - - -); -const TabContent2: ReactNode = ( - <> - Tab Title2 - - Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of - classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a - Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin - words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in - classical literature, discovered the undoubtable source. - - -); - -const TabContent3: ReactNode = ( - <> - Tab Title3 - Tab subTitle3 - - Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of - classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a - Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin - words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in - classical literature, discovered the undoubtable source. - - -); - -const sidebarTab1: SidebarTab = { - id: "1", - label: "Tab1", - content: TabContent, - tabButton1: + // `asChild` allows the user to pass any element as the anchor + if (asChild && isValidElement(children)) { + return cloneElement( + children, + context.getReferenceProps({ + ref, + ...props, + ...children.props, + "data-state": context.open ? "open" : "closed" + }) ); - }, -); + } + + return ( + + ); +}); type ContentProps = { className?: string; attachToRoot?: boolean; }; -export const Content = forwardRef & ContentProps>( - function Content({ style, className, attachToRoot = false, ...props }, propRef) { - const { context: floatingContext, ...context } = usePopoverContext(); - const theme = useTheme(); - const ref = useMergeRefs([context.refs.setFloating, propRef]); - const { isMounted, styles: transitionStyles } = useTransitionStyles(floatingContext, { - duration: 50, - }); +export const Content = forwardRef< + HTMLDivElement, + HTMLProps & ContentProps +>(function Content( + { style, className, attachToRoot = false, ...props }, + propRef +) { + const { context: floatingContext, ...context } = usePopoverContext(); + const theme = useTheme(); + const ref = useMergeRefs([context.refs.setFloating, propRef]); + const { isMounted, styles: transitionStyles } = useTransitionStyles( + floatingContext, + { + duration: 50 + } + ); - if (!isMounted) return null; + if (!isMounted) return null; - return ( - ) : null}> - -
- {props.children} -
-
-
- ); - }, -); + return ( + ) + : null + } + > + +
+ {props.children} +
+
+
+ ); +}); -export const Close = forwardRef>( - function PopoverClose(props, ref) { - const { setOpen } = usePopoverContext(); - return ( - - ); -}; - -type ButtonProps = { - selected?: boolean; -}; - -const Button = styled.button` - display: flex; - flex-direction: row; - align-items: flex-start; - justify-content: center; - padding: 4px 8px; - gap: 8px; - border-radius: 4px; - color: ${({ selected, theme }) => (selected ? theme.content.main : theme.content.weak)}; - background: ${({ disabled, theme }) => (disabled ? theme.bg[3] : theme.bg[0])}; - line-height: 19px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - - :hover { - background: ${({ theme }) => theme.bg[3]}; - color: ${({ theme }) => theme.content.main}; - transition: all 0.4s; - } -`; - -export default TabButton; diff --git a/web/src/beta/components/TabMenu/index.stories.tsx b/web/src/beta/components/TabMenu/index.stories.tsx deleted file mode 100644 index a193758766..0000000000 --- a/web/src/beta/components/TabMenu/index.stories.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { useArgs } from "@storybook/preview-api"; -import { Meta, StoryObj } from "@storybook/react"; -import { ReactNode, CSSProperties } from "react"; - -import Button from "@reearth/beta/components/Button"; -import Resizable from "@reearth/beta/components/Resizable"; -import Text from "@reearth/beta/components/Text"; -import { styled } from "@reearth/services/theme"; - -import TabMenu, { Props } from "./index"; - -export default { - component: TabMenu, -} as Meta; - -type Story = StoryObj; - -const SampleComponent = () => { - return ( -
-
- ); -}; - -const JSONTag = styled.pre` - background: ${({ theme }) => theme.bg[0]}; - color: ${({ theme }) => theme.content.main}; - border-radius: 10px; - padding: 10px; -`; - -const Container: React.FC<{ children?: ReactNode; style?: CSSProperties }> = ({ - children, - style, -}) =>
{children}
; - -const Pane = ( -
- {" "} - Whatever the main area holds. The tab panel works only when on the left with{" "} - flex 100%. Which is to do with Resizable Component and not the left panel. -
-); - -export const Default: Story = (args: Props) => { - const [_, updateArgs] = useArgs(); - - const handleChange = (tab: string) => updateArgs({ selectedTab: tab }); - - return ( - - {Pane} - - - - - ); -}; - -Default.args = { - tabs: [ - { id: "tab1", name: "My infobox", icon: "infobox", component: }, - { - id: "tab2", - icon: "marker", - component: Tab 2. Can be any react component, - }, - ], - selectedTab: "tab1", -}; diff --git a/web/src/beta/components/TabMenu/index.tsx b/web/src/beta/components/TabMenu/index.tsx deleted file mode 100644 index 70bbd1cb85..0000000000 --- a/web/src/beta/components/TabMenu/index.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { FC, useMemo } from "react"; - -import Icon from "@reearth/beta/components/Icon"; -import Icons from "@reearth/beta/components/Icon/icons"; -import { styled } from "@reearth/services/theme"; - -import Text from "../Text"; - -type menuAlignment = "top" | "left"; - -export type TabObject = { - id: string; - name?: string; - icon?: keyof typeof Icons; - component?: JSX.Element; -}; - -export type Props = { - tabs: TabObject[]; - selectedTab: string; - scrollable?: boolean; - onSelectedTabChange: (tab: string) => void; - menuAlignment?: menuAlignment; -}; - -const TabMenu: FC = ({ - tabs, - selectedTab, - scrollable, - onSelectedTabChange, - menuAlignment, -}) => { - const selectedTabItem = useMemo(() => { - return tabs.find(({ id }) => id === selectedTab); - }, [selectedTab, tabs]); - - return ( - - - {tabs.map(({ id, icon, name }) => ( - onSelectedTabChange(id)} - selected={id === selectedTab} - menuAlignment={menuAlignment}> - {icon ? : {name}} - - ))} - - - {selectedTabItem?.name && !menuAlignment && ( -
- {selectedTabItem.name} -
- )} - {selectedTabItem ? selectedTabItem.component : null} -
-
- ); -}; - -export default TabMenu; - -const Wrapper = styled.div<{ menuAlignment?: menuAlignment }>` - display: ${({ menuAlignment }) => (menuAlignment === "top" ? "flex" : "grid")}; - grid-template-columns: 28px 1fr; - flex-flow: column nowrap; - position: relative; - background: ${({ theme }) => theme.bg[1]}; - height: 100%; -`; - -const Tabs = styled.div<{ menuAlignment?: menuAlignment }>` - display: flex; - flex-flow: ${({ menuAlignment }) => (menuAlignment === "top" ? "row" : "column")} nowrap; - padding-top: 4px; - background: ${({ theme }) => theme.bg[0]}; -`; - -const TabIconWrapper = styled.div<{ selected: boolean; menuAlignment?: menuAlignment }>` - padding: 4px; - display: flex; - justify-content: center; - align-items: center; - cursor: pointer; - color: ${({ selected, theme }) => (selected ? theme.content.main : theme.content.weak)}; - background: ${props => (props.selected ? props.theme.bg[1] : "inherit")}; - border-top-right-radius: ${({ menuAlignment }) => (menuAlignment === "top" ? "4px" : "0")}; - border-top-left-radius: ${({ menuAlignment }) => (menuAlignment === "top" ? "4px" : "0")}; -`; - -const TabHeader = styled(Text)` - padding: 4px; -`; - -const Header = styled.div` - padding: 12px; - border-bottom: 1px solid ${({ theme }) => theme.outline.weak}; -`; - -const MainArea = styled.div<{ scrollable?: boolean }>` - display: block; - height: auto; - overflow-y: ${({ scrollable }) => (scrollable ? "auto" : "unset")}; -`; - -const Content = styled.div` - padding: 12px; -`; diff --git a/web/src/beta/components/Text/index.stories.tsx b/web/src/beta/components/Text/index.stories.tsx index 2a7b890e51..27e86e240d 100644 --- a/web/src/beta/components/Text/index.stories.tsx +++ b/web/src/beta/components/Text/index.stories.tsx @@ -3,7 +3,7 @@ import { Meta, StoryObj } from "@storybook/react"; import Text from "."; const meta: Meta = { - component: Text, + component: Text }; export default meta; @@ -21,7 +21,7 @@ export const Regular: Story = { Body Regular Footnote Regular={" "} - ), + ) }; export const Medium: Story = { @@ -49,7 +49,7 @@ export const Medium: Story = { Footnote Medium - ), + ) }; export const Bold: Story = { @@ -77,5 +77,5 @@ export const Bold: Story = { Footnote Bold - ), + ) }; diff --git a/web/src/beta/components/Text/index.tsx b/web/src/beta/components/Text/index.tsx index 62657f4630..523cc6cbc3 100644 --- a/web/src/beta/components/Text/index.tsx +++ b/web/src/beta/components/Text/index.tsx @@ -1,14 +1,18 @@ -import { CSSProperties, ReactNode, useMemo } from "react"; - import { useTheme } from "@reearth/services/theme"; import { FontWeight, typography, FontSize, - UniqueTraits, + UniqueTraits } from "@reearth/services/theme/reearthTheme/common/fonts"; +import { CSSProperties, ReactNode, useMemo } from "react"; -type NonChangeableProperties = "color" | "fontFamily" | "fontSize" | "lineHeight" | "fontStyle"; +type NonChangeableProperties = + | "color" + | "fontFamily" + | "fontSize" + | "lineHeight" + | "fontStyle"; type ChangeableProperties = Omit; @@ -35,7 +39,7 @@ const Text: React.FC = ({ trait, otherProperties, onClick, - onDoubleClick, + onDoubleClick }) => { const theme = useTheme(); const defaultColor = theme.content.main; @@ -46,9 +50,10 @@ const Text: React.FC = ({ trait && typographyBySize && trait in typographyBySize ? typographyBySize[trait] : typographyBySize && weight in typographyBySize - ? typographyBySize[weight] - : typographyBySize && typographyBySize[size === "h1" ? "medium" : "regular"], - [trait, size, typographyBySize, weight], + ? typographyBySize[weight] + : typographyBySize && + typographyBySize[size === "h1" ? "medium" : "regular"], + [trait, size, typographyBySize, weight] ); return Typography ? ( @@ -57,10 +62,11 @@ const Text: React.FC = ({ style={{ userSelect: "none", ...otherProperties, - color: customColor ? undefined : color || defaultColor, + color: customColor ? undefined : color || defaultColor }} onClick={onClick} - onDoubleClick={onDoubleClick}> + onDoubleClick={onDoubleClick} + > {children} ) : null; diff --git a/web/src/beta/components/Toggle/index.stories.tsx b/web/src/beta/components/Toggle/index.stories.tsx deleted file mode 100644 index 6215c19e52..0000000000 --- a/web/src/beta/components/Toggle/index.stories.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { useArgs } from "@storybook/preview-api"; -import { Meta, StoryObj } from "@storybook/react"; -import { useCallback } from "react"; - -import { styled } from "@reearth/services/theme"; - -import Toggle, { Props } from "."; - -const meta: Meta = { - component: Toggle, -}; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = (args: Props) => { - const [_, updateArgs] = useArgs(); - - const handleChange = useCallback( - (checked: boolean) => updateArgs({ checked: !checked }), - [updateArgs], - ); - - return ( - -
- -
-
- -
-
- -
-
- ); -}; - -const Wrapper = styled.div` - display: flex; - flex-direction: column; - gap: 10%; - margin-left: 2rem; - margin-top: 2rem; - height: 300px; -`; - -Default.args = { - checked: true, - disabled: false, - onChange: () => console.log("clicked"), -}; diff --git a/web/src/beta/components/Toggle/index.tsx b/web/src/beta/components/Toggle/index.tsx deleted file mode 100644 index aa49be996c..0000000000 --- a/web/src/beta/components/Toggle/index.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { styled } from "@reearth/services/theme"; - -export type Props = { - checked: boolean; - onChange: (checked: boolean) => void; - disabled?: boolean; -}; - -const Toggle: React.FC = ({ checked, onChange, disabled = false }) => ( - - onChange(!checked)}> - - - -); - -export default Toggle; - -const Wrapper = styled.div` - display: flex; - justify-content: space-between; - align-items: center; -`; - -const Switch = styled.label<{ - checked: boolean; - disabled: boolean; -}>` - cursor: ${({ disabled }) => (!disabled ? "pointer" : "not-allowed")}; - width: 24px; - height: 14px; - background: ${({ checked, theme }) => (checked ? theme.select.main : theme.secondary.main)}; - border: 1px solid ${({ checked, theme }) => (checked ? theme.select.main : theme.secondary.main)}; - border-radius: 12px; - opacity: ${({ disabled }) => (!disabled ? 1 : 0.5)}; - transition: 0.4s; -`; - -const TopSlider = styled.div<{ - checked: boolean; -}>` - width: 14px; - height: 14px; - background: ${({ theme }) => theme.content.withBackground}; - transition: 0.4s; - border-radius: 50%; - transform: ${({ checked }) => checked && "translateX(10px)"}; -`; diff --git a/web/src/beta/components/fields/CameraField/CapturePanel/index.tsx b/web/src/beta/components/fields/CameraField/CapturePanel/index.tsx deleted file mode 100644 index 73f6c458c7..0000000000 --- a/web/src/beta/components/fields/CameraField/CapturePanel/index.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { useMemo } from "react"; - -import Button from "@reearth/beta/components/Button"; -import NumberInput from "@reearth/beta/components/fields/common/NumberInput"; -import Text from "@reearth/beta/components/Text"; -import { Camera } from "@reearth/beta/utils/value"; -import { useT } from "@reearth/services/i18n"; -import { styled } from "@reearth/services/theme"; - -import PanelCommon from "../../common/PanelCommon"; -import type { RowType } from "../types"; -import { radiansToDegrees } from "../utils"; - -type Props = { - camera?: Camera; - onSave: (value?: Camera) => void; - onClose: () => void; -}; - -const CapturePanel: React.FC = ({ camera, onSave, onClose }) => { - const t = useT(); - - const panelContent: { [key: string]: RowType } = useMemo(() => { - return { - [t("Current Position")]: [ - { id: "lat", description: t("Latitude"), value: camera?.lat }, - { id: "lng", description: t("Longitude"), value: camera?.lng }, - { id: "height", description: t("Height"), value: camera?.height }, - ], - [t("Current Rotation")]: [ - { id: "heading", description: t("Heading"), value: radiansToDegrees(camera?.heading ?? 0) }, - { id: "pitch", description: t("Pitch"), value: radiansToDegrees(camera?.pitch ?? 0) }, - { id: "roll", description: t("Roll"), value: radiansToDegrees(camera?.roll ?? 0) }, - ], - }; - }, [t, camera]); - - return ( - - {Object.keys(panelContent).map(group => ( - - {group} - - {panelContent[group].map(field => ( - - ))} - - - ))} - - - - onSave(camera)} - /> - - - ); -}; - -export default CapturePanel; - -const FieldGroup = styled.div` - display: flex; - justify-content: space-between; - flex-direction: column; - gap: 4px; - padding: 4px; -`; - -const InputWrapper = styled.div` - display: flex; - gap: 4px; -`; - -const Divider = styled.div` - border-top: 1px solid ${({ theme }) => theme.outline.weak}; -`; - -const ButtonWrapper = styled.div` - display: flex; - gap: 8px; - padding: 8px; -`; - -const StyledButton = styled(Button)` - flex: 1; -`; - -const StyledNumberInput = styled(NumberInput)` - flex: 1; -`; diff --git a/web/src/beta/components/fields/CameraField/EditPanel/hooks.ts b/web/src/beta/components/fields/CameraField/EditPanel/hooks.ts deleted file mode 100644 index b9540d0cf3..0000000000 --- a/web/src/beta/components/fields/CameraField/EditPanel/hooks.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; - -import { useT } from "@reearth/services/i18n"; - -import type { Camera, RowType } from "../types"; -import { saveFriendlyCamera, userFriendlyCamera } from "../utils"; - -export default ({ - camera, - onFlyTo, - onSave, -}: { - camera?: Camera; - onFlyTo?: (c?: Camera) => void; - onSave: (value?: Camera) => void; -}) => { - const t = useT(); - const [newCamera, setNewCamera] = useState( - camera ? userFriendlyCamera(camera) : undefined, - ); - - useEffect(() => { - if (!newCamera && camera) { - setNewCamera(userFriendlyCamera(camera)); - } - }, [newCamera, camera]); - - const handleFieldUpdate = useCallback( - (key: keyof Camera, update?: number) => { - if (update === undefined || !newCamera) return; - const updated: Camera = { - ...newCamera, - [key]: update, - }; - setNewCamera(updated); - onFlyTo?.(saveFriendlyCamera(updated)); - }, - [newCamera, onFlyTo], - ); - - const panelContent: { [key: string]: RowType } = useMemo(() => { - return { - [t("Location")]: [ - { id: "lat", description: t("Latitude") }, - { id: "lng", description: t("Longitude") }, - ], - [t("Height")]: [{ id: "height", unit: "km", description: t("Height") }], - [t("Rotation")]: [ - { id: "heading", description: t("Heading") }, - { id: "pitch", description: t("Pitch") }, - { id: "roll", description: t("Roll") }, - ], - }; - }, [t]); - - const handleChange = useCallback( - (field: keyof Camera) => (value?: number) => { - if (value === newCamera?.[field]) return; - handleFieldUpdate(field, value); - }, - [newCamera, handleFieldUpdate], - ); - - const handleSave = useCallback(() => { - if (!newCamera) return; - const saveFriendly = saveFriendlyCamera(newCamera); - onSave?.(saveFriendly); - }, [newCamera, onSave]); - - return { - newCamera, - panelContent, - handleChange, - handleSave, - }; -}; diff --git a/web/src/beta/components/fields/CameraField/EditPanel/index.tsx b/web/src/beta/components/fields/CameraField/EditPanel/index.tsx deleted file mode 100644 index 84945e7178..0000000000 --- a/web/src/beta/components/fields/CameraField/EditPanel/index.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import Button from "@reearth/beta/components/Button"; -import NumberInput from "@reearth/beta/components/fields/common/NumberInput"; -import Text from "@reearth/beta/components/Text"; -import type { Camera } from "@reearth/beta/utils/value"; -import { useT } from "@reearth/services/i18n"; -import { styled } from "@reearth/services/theme"; - -import PanelCommon from "../../common/PanelCommon"; - -import useHooks from "./hooks"; - -type Props = { - camera?: Camera; - onSave: (value?: Camera) => void; - onFlyTo?: (camera?: Camera) => void; - onClose: () => void; -}; - -const EditPanel: React.FC = ({ camera, onSave, onFlyTo, onClose }) => { - const t = useT(); - - const { newCamera, panelContent, handleChange, handleSave } = useHooks({ - camera, - onFlyTo, - onSave, - }); - - return ( - - {Object.keys(panelContent).map(group => ( - - {group} - - {panelContent[group].map(field => ( - - ))} - - - ))} - - - - - - - ); -}; - -export default EditPanel; - -const FieldGroup = styled.div` - display: flex; - justify-content: space-between; - flex-direction: column; - gap: 4px; - padding: 4px; -`; - -const InputWrapper = styled.div` - display: flex; - gap: 4px; -`; - -const Divider = styled.div` - border-top: 1px solid ${({ theme }) => theme.outline.weak}; -`; - -const ButtonWrapper = styled.div` - display: flex; - gap: 8px; - padding: 8px; -`; - -const StyledButton = styled(Button)` - flex: 1; -`; - -const StyledNumberInput = styled(NumberInput)` - flex: 1; -`; diff --git a/web/src/beta/components/fields/CameraField/index.stories.tsx b/web/src/beta/components/fields/CameraField/index.stories.tsx deleted file mode 100644 index a2f45ff4dd..0000000000 --- a/web/src/beta/components/fields/CameraField/index.stories.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { useArgs } from "@storybook/preview-api"; -import { Meta, StoryObj } from "@storybook/react"; -import { useCallback } from "react"; - -import { Camera } from "@reearth/beta/utils/value"; -import { styled } from "@reearth/services/theme"; - -import CameraField, { type Props } from "."; - -const meta: Meta = { - component: CameraField, -}; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = (args: Props) => { - const [_, updateArgs] = useArgs(); - - const handleSave = useCallback((value?: Camera) => updateArgs({ value: value }), [updateArgs]); - - const handleFlyTo = useCallback(() => updateArgs({ value: undefined }), [updateArgs]); - - return ( - -
- -
-
- -
-
- -
-
- ); -}; - -Default.args = { - name: "Camera field", - description: "Camera field description", - value: undefined, - disabled: false, - onSave: (value?: Camera) => console.log("saved camera value: ", value), - onFlyTo: target => console.log("Fly to", target), -}; - -const Wrapper = styled.div` - display: flex; - flex-direction: column; - gap: 10%; - margin-left: 2rem; - margin-top: 2rem; - height: 300px; -`; diff --git a/web/src/beta/components/fields/CameraField/index.tsx b/web/src/beta/components/fields/CameraField/index.tsx deleted file mode 100644 index dc03d12a6e..0000000000 --- a/web/src/beta/components/fields/CameraField/index.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import { useCallback, useState } from "react"; - -import Button from "@reearth/beta/components/Button"; -import Icon from "@reearth/beta/components/Icon"; -import * as Popover from "@reearth/beta/components/Popover"; -import Text from "@reearth/beta/components/Text"; -// import Slider from "@reearth/beta/components/Slider"; -import { useCurrentCamera } from "@reearth/beta/features/Editor/atoms"; -import type { Camera } from "@reearth/beta/utils/value"; -import type { FlyTo } from "@reearth/core"; -import { useT } from "@reearth/services/i18n"; -import { styled } from "@reearth/services/theme"; - -import Property from ".."; - -import CapturePanel from "./CapturePanel"; -import EditPanel from "./EditPanel"; - -type Panel = "editor" | "capture" | undefined; - -export type Props = { - name?: string; - description?: string; - value?: Camera; - disabled?: boolean; - onSave: (value?: Camera) => void; - onFlyTo?: FlyTo; -}; - -const CameraField: React.FC = ({ name, description, value, disabled, onSave, onFlyTo }) => { - const t = useT(); - const [open, setOpen] = useState(undefined); - - const handleClose = useCallback(() => setOpen(undefined), []); - - const [currentCamera] = useCurrentCamera(); - - const handleClick = useCallback( - (panel: Panel) => setOpen(current => (current === panel ? undefined : panel)), - [], - ); - - const handleSave = useCallback( - (value?: Camera) => { - onSave(value); - setOpen(undefined); - }, - [onSave], - ); - - const handleFlyto = useCallback( - (c?: Partial) => { - const dest = c ?? currentCamera; - if (dest) { - onFlyTo?.(dest); - } - }, - [currentCamera, onFlyTo], - ); - - const handleRemoveSetting = useCallback(() => { - if (!value) return; - handleSave(); - }, [value, handleSave]); - - return ( - - - - - - {value && ( - handleFlyto(value)} /> - )} - - {value ? t("Position Set") : t("Not set")} - - - - handleClick("editor")} - disabled={disabled} - /> - handleClick("capture")} - disabled={disabled} - /> - - - - {open === "capture" ? ( - - ) : open === "editor" ? ( - - ) : null} - - - - ); -}; - -const InputWrapper = styled.div<{ disabled?: boolean }>` - display: flex; - gap: 10px; - - flex-wrap: wrap; - opacity: ${({ disabled }) => (disabled ? 0.6 : 1)}; -`; - -const Input = styled.div<{ positionSet?: boolean }>` - display: flex; - align-items: center; - justify-content: space-between; - gap: 4px; - flex: 1; - padding: 0 8px; - height: 28px; - border-radius: 4px; - border: 1px solid ${({ theme }) => theme.outline.weak}; - color: ${({ theme }) => theme.content.main}; - background: ${({ theme }) => theme.bg[1]}; - box-shadow: ${({ theme }) => theme.shadow.input}; - - color: ${({ theme, positionSet }) => (positionSet ? theme.content.main : theme.content.weak)}; -`; - -const StyledText = styled(Text)` - white-space: nowrap; -`; - -const TriggerButton = styled(Button)` - height: 28px; - margin: 0; -`; - -const ZoomToIcon = styled(Icon)` - :hover { - cursor: pointer; - } -`; - -const DeleteIcon = styled(Icon)<{ disabled?: boolean }>` - ${({ disabled, theme }) => - disabled - ? `color: ${theme.content.weaker};` - : `:hover { - cursor: pointer; - }`} -`; - -export default CameraField; diff --git a/web/src/beta/components/fields/CameraField/types.ts b/web/src/beta/components/fields/CameraField/types.ts deleted file mode 100644 index 25297cb761..0000000000 --- a/web/src/beta/components/fields/CameraField/types.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { Camera } from "@reearth/beta/utils/value"; - -export type { Camera } from "@reearth/beta/utils/value"; - -export type RowType = { - id: keyof Camera; - value?: number; - description?: string; - suffix?: string; - unit?: string; -}[]; diff --git a/web/src/beta/components/fields/CameraField/utils.ts b/web/src/beta/components/fields/CameraField/utils.ts deleted file mode 100644 index c0d081bf52..0000000000 --- a/web/src/beta/components/fields/CameraField/utils.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Camera } from "./types"; - -export const degreesToRadians = (degrees: number): number => degrees * (Math.PI / 180); - -export const radiansToDegrees = (radians: number): number => radians * (180 / Math.PI); - -export const userFriendlyCamera = (camera: Camera) => ({ - lat: camera.lat, - lng: camera.lng, - height: camera.height, - heading: radiansToDegrees(camera.heading), - pitch: radiansToDegrees(camera.pitch), - roll: radiansToDegrees(camera.roll), - fov: camera?.fov, -}); - -export const saveFriendlyCamera = (camera: Camera) => ({ - lat: camera.lat, - lng: camera.lng, - height: camera.height, - heading: degreesToRadians(camera.heading), - pitch: degreesToRadians(camera.pitch), - roll: degreesToRadians(camera.roll), - fov: camera?.fov, -}); diff --git a/web/src/beta/components/fields/ColorField/hooks.ts b/web/src/beta/components/fields/ColorField/hooks.ts deleted file mode 100644 index a5b0b12c3c..0000000000 --- a/web/src/beta/components/fields/ColorField/hooks.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { useState, useEffect, useCallback, useRef } from "react"; -import tinycolor from "tinycolor2"; - -import { Params, RGBA } from "./types"; -import { getChannelLabel, getChannelValue, getHexString } from "./utils"; - -export default ({ value, onChange }: Params) => { - const [colorState, setColor] = useState(); - const [rgba, setRgba] = useState(tinycolor(value).toRgb()); - const [tempColor, setTempColor] = useState(colorState); - const [open, setOpen] = useState(false); - - const wrapperRef = useRef(null); - const pickerRef = useRef(null); - - //Actions - - const handleChange = useCallback((newColor: RGBA) => { - const color = getHexString(newColor); - if (!color) return; - setTempColor(color); - setRgba(newColor); - }, []); - - const handleHexInput = useCallback( - (e: React.ChangeEvent) => { - e.preventDefault(); - setColor(e.target.value); - setRgba(tinycolor(e.target.value ?? colorState).toRgb()); - }, - [colorState], - ); - - const handleRgbaInput = useCallback( - (e: React.ChangeEvent) => { - e.preventDefault(); - - handleChange({ - ...rgba, - [e.target.name]: e.target.value ? Number(e.target.value) : undefined, - }); - }, - [handleChange, rgba], - ); - - const handleClose = useCallback(() => { - if (value || colorState) { - setColor(value ?? colorState); - setRgba(tinycolor(value ?? colorState).toRgb()); - } else { - setColor(undefined); - setRgba(tinycolor(colorState == null ? undefined : colorState).toRgb()); - } - setTempColor(undefined); - setOpen(false); - }, [value, colorState]); - - const handleSave = useCallback(() => { - if (!onChange) return; - if (tempColor && tempColor != value && tempColor != colorState) { - setColor(tempColor); - setRgba(tinycolor(tempColor).toRgb()); - onChange(tempColor); - setTempColor(undefined); - } else if (colorState != value) { - onChange(colorState); - } - setOpen(false); - }, [colorState, onChange, tempColor, value]); - - const handleHexSave = useCallback(() => { - const hexPattern = /^#?([a-fA-F0-9]{3,4}|[a-fA-F0-9]{6}|[a-fA-F0-9]{8})$/; - if (!colorState || hexPattern.test(colorState)) { - handleSave(); - } else { - value && setColor(value); - } - }, [colorState, handleSave, value]); - - //events - - const handleClick = useCallback(() => { - setOpen(!open); - }, [open]); - - const handleKeyPress = useCallback( - (e: React.KeyboardEvent) => { - if (e.key === "Enter") { - handleHexSave(); - } - }, - [handleHexSave], - ); - - //UseEffects - - useEffect(() => { - if (value) { - setColor(value); - setRgba(tinycolor(value).toRgb()); - } else { - setColor(undefined); - } - }, [value]); - - useEffect(() => { - const handleClickOutside = (e: MouseEvent | TouchEvent) => { - if (open && wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) { - if (colorState != value && !open) { - handleSave(); - } - handleClose(); - setOpen(false); - } - }; - - document.addEventListener("mousedown", handleClickOutside); - document.addEventListener("touchstart", handleClickOutside); - - return () => { - document.removeEventListener("mousedown", handleClickOutside); - document.removeEventListener("touchstart", handleClickOutside); - }; - }, []); // eslint-disable-line react-hooks/exhaustive-deps - - return { - wrapperRef, - pickerRef, - colorState, - open, - rgba, - getChannelLabel, - getChannelValue, - handleClose, - handleSave, - handleHexSave, - handleChange, - handleRgbaInput, - handleHexInput, - handleClick, - handleKeyPress, - }; -}; diff --git a/web/src/beta/components/fields/ColorField/index.stories.tsx b/web/src/beta/components/fields/ColorField/index.stories.tsx deleted file mode 100644 index 8ff395bf12..0000000000 --- a/web/src/beta/components/fields/ColorField/index.stories.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { action } from "@storybook/addon-actions"; -import { Meta, StoryObj } from "@storybook/react"; - -import ColorField from "."; - -const meta: Meta = { - component: ColorField, -}; - -export default meta; - -type Story = StoryObj; - -export const ColorFieldInput: Story = { - render: () => , -}; diff --git a/web/src/beta/components/fields/ColorField/index.tsx b/web/src/beta/components/fields/ColorField/index.tsx deleted file mode 100644 index 4ca84c92b9..0000000000 --- a/web/src/beta/components/fields/ColorField/index.tsx +++ /dev/null @@ -1,285 +0,0 @@ -import React from "react"; -import { RgbaColorPicker } from "react-colorful"; - -import Button from "@reearth/beta/components/Button"; -import Icon from "@reearth/beta/components/Icon"; -import * as Popover from "@reearth/beta/components/Popover"; -import Text from "@reearth/beta/components/Text"; -import { useT } from "@reearth/services/i18n"; -import { styled, css, useTheme } from "@reearth/services/theme"; - -import Property from ".."; - -import useHooks from "./hooks"; -import { Props, RGBA } from "./types"; - -const channels = ["r", "g", "b", "a"]; -const hexPlaceholder = "#RRGGBBAA"; - -const ColorField: React.FC = ({ name, description, value, onChange, className }) => { - const t = useT(); - const theme = useTheme(); - const { - wrapperRef, - pickerRef, - colorState, - open, - rgba, - getChannelValue, - handleClose, - handleSave, - handleHexSave, - handleChange, - handleRgbaInput, - handleHexInput, - handleClick, - handleKeyPress, - } = useHooks({ value, onChange }); - - return ( - - - - - - - - - - - - - - - - {t("Color Picker")} - - {handleClose && } - - - - - RGBA - - {channels.map(channel => ( - - ))} - - - - - - - - - - - - ); -}; - -const Wrapper = styled.div` - text-align: center; - width: 100%; - cursor: pointer; -`; - -const InputWrapper = styled.div` - display: flex; - gap: 4px; - background: ${({ theme }) => theme.bg[1]}; -`; - -const Layers = styled.div` - position: relative; - min-width: 28px; - min-height: 28px; - border-radius: 4px; - border: 1px solid ${({ theme }) => theme.outline.weak}; - box-shadow: ${({ theme }) => theme.shadow.input}; -`; - -const layerStyle = css` - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; -`; - -const check = (color: string) => ` -linear-gradient( - 45deg, - ${color} 25%, - transparent 25%, - transparent 75%, - ${color} 25%, - ${color} -) -`; - -const CheckedPattern = styled.div` - background-color: ${({ theme }) => theme.outline.main}; - background-image: ${({ theme }) => check(theme.bg[3])}, ${({ theme }) => check(theme.bg[3])}; - background-position: 0 0, 6px 6px; - background-size: 12px 12px; - ${layerStyle}; -`; - -const Swatch = styled.div<{ c?: string }>` - background: ${({ c }) => c || "transparent"}; - ${layerStyle}; -`; - -const PickerWrapper = styled(Popover.Content)` - width: 286px; - height: 362px; - border: 1px solid ${({ theme }) => theme.outline.weak}; - border-radius: 4px; - background: ${({ theme }) => theme.bg[1]}; - box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.5); -`; - -const HeaderWrapper = styled.div` - display: flex; - padding: 4px 8px; - justify-content: space-between; - align-items: center; - gap: 10px; - height: 28px; - border-bottom: 1px solid ${({ theme }) => theme.outline.weak}; -`; - -const PickerTitle = styled(Text)` - text-align: center; - margin-right: auto; -`; - -const CloseIcon = styled(Icon)` - margin-left: auto; - cursor: pointer; -`; - -const SelectorPickerWrapper = styled.div` - display: flex; - padding: 8px; - flex-direction: column; - align-items: flex-start; -`; - -const ValuesWrapper = styled.div` - display: flex; - flex-direction: row; - align-items: flex-start; - gap: 4px; - padding-top: 8px; - padding-bottom: 8px; -`; - -const Input = styled.input<{ type?: string }>` - display: flex; - padding: 4px 8px; - align-items: center; - margin: 0 auto; - border-radius: 4px; - gap: 4px; - border: 1px solid ${({ theme }) => theme.outline.weak}; - color: ${({ theme }) => theme.content.main}; - background: ${({ theme }) => theme.bg[1]}; - box-shadow: ${({ theme }) => theme.shadow.input}; - box-sizing: border-box; - outline: none; - &:focus { - border-color: ${({ theme }) => theme.outline.main}; - } - width: 100%; - height: 30px; -`; - -const RgbaInputWrapper = styled.div` - display: flex; - height: 56px; - flex-direction: column; - align-items: flex-start; - justify-content: flex-start; - gap: 8px; -`; -const FormButtonGroup = styled.div` - display: flex; - flex-direction: row; - height: 28px; - justify-content: center; - border-top: 1px solid ${({ theme }) => theme.bg[3]}; - padding: 8px; - gap: 8px; -`; -const ButtonWrapper = styled(Button)` - height: 27px; - min-width: 135px; - padding: 0px; - margin: 0px; -`; - -const ColorPickerStyles = css` - padding: 8px 8px 6px 0px; - gap: 12px; - - .react-colorful__saturation-pointer { - width: 15px; - height: 15px; - border-width: 2px; - } - - .react-colorful__hue-pointer, - .react-colorful__alpha-pointer { - width: 1px; - height: 10px; - border: 1px solid white; - border-radius: 2px; - } - - .react-colorful__saturation { - margin-bottom: 10px; - border-radius: 3px; - width: 270px; - border-bottom: none; - } - - .react-colorful__hue, - .react-colorful__alpha { - height: 10px; - width: 270px; - margin: 0 5px 10px 3px; - border-radius: 3px; - } -`; - -const ColorPicker = styled(RgbaColorPicker)` - ${ColorPickerStyles} -`; - -export default ColorField; diff --git a/web/src/beta/components/fields/ColorField/types.ts b/web/src/beta/components/fields/ColorField/types.ts deleted file mode 100644 index 5f253e1e8b..0000000000 --- a/web/src/beta/components/fields/ColorField/types.ts +++ /dev/null @@ -1,20 +0,0 @@ -// Component Props -export type Props = { - className?: string; - name?: string; - description?: string; - value?: string; - onChange?: (value?: string) => void; -}; - -export type RGBA = { - r: number; - g: number; - b: number; - a: number; -}; - -export type Params = { - value?: string; - onChange?: (value?: string) => void; -}; diff --git a/web/src/beta/components/fields/ColorField/utils.ts b/web/src/beta/components/fields/ColorField/utils.ts deleted file mode 100644 index 87322e43fa..0000000000 --- a/web/src/beta/components/fields/ColorField/utils.ts +++ /dev/null @@ -1,28 +0,0 @@ -import tinycolor, { ColorInput } from "tinycolor2"; - -import { RGBA } from "./types"; - -export const getHexString = (value?: ColorInput) => { - if (!value) return undefined; - const color = tinycolor(value); - return color.getAlpha() === 1 ? color.toHexString() : color.toHex8String(); -}; - -export const getChannelLabel = (channel: string) => { - switch (channel) { - case "r": - return "Red"; - case "g": - return "Green"; - case "b": - return "Blue"; - case "a": - return "Alpha"; - default: - return ""; - } -}; - -export function getChannelValue(rgba: RGBA, channel: keyof RGBA): number { - return rgba[channel]; -} diff --git a/web/src/beta/components/fields/DateTimeField/EditPanel/hooks.ts b/web/src/beta/components/fields/DateTimeField/EditPanel/hooks.ts deleted file mode 100644 index 40122617e7..0000000000 --- a/web/src/beta/components/fields/DateTimeField/EditPanel/hooks.ts +++ /dev/null @@ -1,96 +0,0 @@ -import moment from "moment-timezone"; -import { useCallback, useEffect, useMemo, useState } from "react"; - -import { getUniqueTimezones } from "@reearth/beta/utils/moment-timezone"; - -type Props = { - value?: string; - onChange?: (value?: string | undefined) => void; - setDateTime?: (value?: string | undefined) => void; -}; - -type TimezoneInfo = { - timezone: string; - offset: string; -}; - -export default ({ value, onChange, setDateTime }: Props) => { - const [date, setDate] = useState(""); - const [time, setTime] = useState(""); - const [selectedTimezone, setSelectedTimezone] = useState({ - offset: "+00:00", - timezone: "Africa/Abidjan", - }); - - const handleTimeChange = useCallback((newValue: string | undefined) => { - if (newValue === undefined) return; - setTime(newValue); - }, []); - - const handleDateChange = useCallback((newValue: string | undefined) => { - if (newValue === undefined) return; - setDate(newValue); - }, []); - - const offsetFromUTC: TimezoneInfo[] = useMemo(() => { - return getUniqueTimezones(moment.tz.names()); - }, []); - - const handleApplyChange = useCallback(() => { - const selectedTimezoneInfo = offsetFromUTC.find( - info => info.timezone === selectedTimezone.timezone, - ); - if (selectedTimezoneInfo) { - const formattedDateTime = `${date}T${time}${selectedTimezoneInfo.offset}`; - setDateTime?.(formattedDateTime); - onChange?.(formattedDateTime); - } - }, [offsetFromUTC, selectedTimezone.timezone, date, time, setDateTime, onChange]); - - const handleTimezoneSelect = useCallback( - (newValue: string) => { - const updatedTimezone = offsetFromUTC.find(info => info.timezone === newValue); - setSelectedTimezone(updatedTimezone || selectedTimezone); - }, - [offsetFromUTC, selectedTimezone], - ); - - useEffect(() => { - if (value) { - const [parsedDate, timeWithOffset] = value.split("T"); - const [parsedTime, timezoneOffset] = timeWithOffset.split(/[-+]/); - const [timezoneOffsetHour, timezoneOffsetMinute] = timezoneOffset.split(":"); - const formattedTimezoneOffset = - timezoneOffsetHour.length === 2 - ? timezoneOffset - : `${timezoneOffsetHour.padStart(2, "0")}:${timezoneOffsetMinute}`; - - setTime(parsedTime); - setDate(parsedDate); - - const updatedTimezone = offsetFromUTC.find( - info => - info.offset === - (timeWithOffset.includes("-") - ? `-${formattedTimezoneOffset}` - : `+${formattedTimezoneOffset}`), - ); - updatedTimezone && setSelectedTimezone(updatedTimezone); - } else { - setDate(""); - setTime(""); - setSelectedTimezone({ offset: "+00:00", timezone: "Africa/Abidjan" }); - } - }, [value, offsetFromUTC]); - - return { - date, - time, - selectedTimezone, - offsetFromUTC, - handleTimeChange, - onTimezoneSelect: handleTimezoneSelect, - onDateChange: handleDateChange, - onDateTimeApply: handleApplyChange, - }; -}; diff --git a/web/src/beta/components/fields/DateTimeField/EditPanel/index.tsx b/web/src/beta/components/fields/DateTimeField/EditPanel/index.tsx deleted file mode 100644 index e33cafdcdd..0000000000 --- a/web/src/beta/components/fields/DateTimeField/EditPanel/index.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { useMemo } from "react"; - -import Button from "@reearth/beta/components/Button"; -import PanelCommon from "@reearth/beta/components/fields/common/PanelCommon"; -import { useT } from "@reearth/services/i18n"; -import { styled } from "@reearth/services/theme"; - -import TextInput from "../../common/TextInput"; -import SelectField from "../../SelectField"; - -import useHooks from "./hooks"; - -type Props = { - onChange?: (value?: string | undefined) => void; - onClose: () => void; - value?: string; - setDateTime?: (value?: string | undefined) => void; -}; - -const EditPanel: React.FC = ({ onChange, onClose, value, setDateTime }) => { - const t = useT(); - - const { - date, - time, - selectedTimezone, - offsetFromUTC, - onDateChange, - onTimezoneSelect, - onDateTimeApply, - handleTimeChange, - } = useHooks({ value, onChange, setDateTime }); - - const isButtonDisabled = useMemo(() => { - return date.trim() === "" || time.trim() === ""; - }, [date, time]); - - return ( - - - - - - - - - - - - - - ({ - key: timezone.timezone, - label: timezone?.offset, - }))} - onChange={onTimezoneSelect} - /> - - - - - - { - onDateTimeApply(), onClose(); - }} - disabled={isButtonDisabled} - /> - - - ); -}; - -const TextWrapper = styled.div` - margin-left: 8px; - width: 88%; -`; - -const Input = styled(TextInput)` - width: 100%; -`; - -const FieldGroup = styled.div` - padding-bottom: 8px; -`; - -const Label = styled.div` - font-size: 12px; - padding: 10px 0; -`; - -const Divider = styled.div` - border-top: 1px solid ${({ theme }) => theme.outline.weak}; -`; - -const ButtonWrapper = styled.div` - display: flex; - gap: 8px; - padding: 8px; -`; - -const StyledButton = styled(Button)` - flex: 1; -`; - -const SelectWrapper = styled.div` - margin-left: 8px; - width: 95%; -`; -const CustomSelect = styled(SelectField)` - width: 100%; -`; -export default EditPanel; diff --git a/web/src/beta/components/fields/DateTimeField/index.stories.tsx b/web/src/beta/components/fields/DateTimeField/index.stories.tsx deleted file mode 100644 index 8d924477bd..0000000000 --- a/web/src/beta/components/fields/DateTimeField/index.stories.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { action } from "@storybook/addon-actions"; -import { Meta, StoryObj } from "@storybook/react"; - -import DateTimeField from "."; - -const meta: Meta = { - component: DateTimeField, -}; - -export default meta; - -type Story = StoryObj; - -export const DateTimeFieldInput: Story = { - render: () => , -}; diff --git a/web/src/beta/components/fields/DateTimeField/index.tsx b/web/src/beta/components/fields/DateTimeField/index.tsx deleted file mode 100644 index 197362a143..0000000000 --- a/web/src/beta/components/fields/DateTimeField/index.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { useCallback, useEffect, useState } from "react"; - -import Button from "@reearth/beta/components/Button"; -import Icon from "@reearth/beta/components/Icon"; -import * as Popover from "@reearth/beta/components/Popover"; -import Text from "@reearth/beta/components/Text"; -import { useT } from "@reearth/services/i18n"; -import { styled } from "@reearth/services/theme"; - -import Property from ".."; - -import EditPanel from "./EditPanel"; - -export type Props = { - name?: string; - description?: string; - value?: string; - disableField?: boolean; - fieldName?: string; - onChange?: (value?: string | undefined) => void; - onPopoverOpen?: (fieldId?: string) => void; - setDisabledFields?: (value: string[]) => void; -}; - -const DateTimeField: React.FC = ({ - name, - description, - value, - disableField, - fieldName, - onChange, - onPopoverOpen, - setDisabledFields, -}) => { - const [open, setOpen] = useState(false); - const t = useT(); - - const handlePopOver = useCallback(() => { - if (disableField) { - setOpen(false); - } else { - onPopoverOpen?.(fieldName); - setOpen(!open); - } - if (open) setDisabledFields?.([]); - }, [disableField, open, onPopoverOpen, setDisabledFields, fieldName]); - - const handleRemoveSetting = useCallback(() => { - if (!value) return; - setDateTime(""); - onChange?.(); - }, [value, onChange]); - - const [dateTime, setDateTime] = useState(value); - - useEffect(() => { - setDateTime(value); - }, [value]); - - return ( - - - - - - - {dateTime ? dateTime : "YYYY-MM-DDThh:mm:ss±hh:mm"} - - - - handlePopOver()} - /> - - - - {open && ( - - )} - - - - ); -}; - -export default DateTimeField; - -const InputWrapper = styled.div<{ disabled?: boolean }>` - display: flex; - width: 100%; - flex-wrap: wrap; -`; - -const Input = styled.div<{ dataTimeSet?: boolean }>` - display: flex; - align-items: center; - justify-content: space-between; - gap: 4px; - flex: 1; - padding: 0 8px; - border-radius: 4px; - border: 1px solid ${({ theme }) => theme.outline.weak}; - color: ${({ theme }) => theme.content.strong}; - background: ${({ theme }) => theme.bg[1]}; - box-shadow: ${({ theme }) => theme.shadow.input}; - width: 65%; - color: ${({ theme, dataTimeSet }) => (dataTimeSet ? theme.content.strong : theme.content.weak)}; -`; - -const StyledText = styled(Text)` - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - width: 100%; - padding: 4px 0; -`; - -const TriggerButton = styled(Button)` - margin-left: 10px; -`; - -const DeleteIcon = styled(Icon)<{ disabled?: boolean }>` - ${({ disabled, theme }) => - disabled - ? `color: ${theme.content.weaker};` - : `:hover { - cursor: pointer; - }`} -`; diff --git a/web/src/beta/components/fields/ListField/index.stories.tsx b/web/src/beta/components/fields/ListField/index.stories.tsx deleted file mode 100644 index 9dba7cae23..0000000000 --- a/web/src/beta/components/fields/ListField/index.stories.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { useArgs } from "@storybook/preview-api"; -import { Meta, StoryObj } from "@storybook/react"; -import { useCallback } from "react"; - -import { styled } from "@reearth/services/theme"; - -import ListField, { Props } from "."; - -const meta: Meta = { - component: ListField, -}; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = (args: Props) => { - const [_, updateArgs] = useArgs(); - - const addItem = useCallback(() => { - const randomId = (Math.random() + 1).toString(36).substring(7); - updateArgs({ - items: [ - ...args.items, - { - id: randomId, - value: `Item ${randomId}`, - }, - ], - }); - }, [updateArgs, args.items]); - - const removeItem = useCallback( - (key: string) => { - updateArgs({ items: args.items.filter(({ id }) => id != key) }); - }, - [updateArgs, args.items], - ); - - const onItemDrop = useCallback( - (item: { id: string; value: string }, index: number) => { - const items = [...args.items]; - items.splice( - items.findIndex(x => x.id === item.id), - 1, - ); - items.splice(index, 0, item); - updateArgs({ items }); - }, - [updateArgs, args.items], - ); - - const onSelect = useCallback((id: string) => updateArgs({ selected: id }), [updateArgs]); - - return ( - -
- -
-
- ); -}; - -const Wrapper = styled.div` - display: flex; - flex-direction: column; - gap: 10%; - margin-left: 2rem; - margin-top: 2rem; - height: 300px; - width: 300px; -`; - -Default.args = { - name: "List Field", - description: "List field Sample description", - items: [ - { - id: "w3tlwi", - value: "Item w3tlwi", - }, - { - id: "77eg5", - value: "Item 77eg5", - }, - { - id: "7p218", - value: "Item 7p218", - }, - { - id: "xquyo", - value: "Item xquyo", - }, - { - id: "2mewj", - value: "Item 2mewj", - }, - { - id: "d2gmu", - value: "Item d2gmu", - }, - ], -}; diff --git a/web/src/beta/components/fields/ListField/index.tsx b/web/src/beta/components/fields/ListField/index.tsx deleted file mode 100644 index 78ef434c42..0000000000 --- a/web/src/beta/components/fields/ListField/index.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { useCallback, useEffect, useMemo } from "react"; - -import Button from "@reearth/beta/components/Button"; -import DragAndDropList, { - Props as DragAndDropProps, -} from "@reearth/beta/components/DragAndDropList"; -import Property from "@reearth/beta/components/fields"; -import Text from "@reearth/beta/components/Text"; -import { useT } from "@reearth/services/i18n"; -import { styled } from "@reearth/services/theme"; - -type ListItem = { - id: string; - value: string; -}; - -export type Props = { - className?: string; - name?: string; - description?: string; - items: ListItem[]; - removeItem: (id: string) => void; - addItem: () => void; - onSelect: (id: string) => void; - selected?: string; - atLeastOneItem?: boolean; -} & Pick; - -const ListField: React.FC = ({ - className, - name, - description, - items, - removeItem, - addItem, - onItemDrop, - onSelect, - selected, - atLeastOneItem, -}: Props) => { - const t = useT(); - - const deleteItem = useCallback(() => { - if (!selected) return; - removeItem(selected); - }, [selected, removeItem]); - - const getId = useCallback(({ id }: ListItem) => { - return id; - }, []); - - const disableRemoveButton = useMemo(() => { - if (!selected || (atLeastOneItem && items.length === 1)) return true; - - return !items.find(({ id }) => id == selected); - }, [items, selected, atLeastOneItem]); - - // if atleastOneItem is true, make sure one item is always selected - useEffect(() => { - if (!atLeastOneItem) return; - - const updateSelected = !selected || !items.find(({ id }) => id === selected); - if (updateSelected) { - onSelect(items[0]?.id); - } - }, [selected, items, atLeastOneItem, onSelect]); - - return ( - - - - uniqueKey="ListField" - items={items} - onItemDrop={onItemDrop} - getId={getId} - renderItem={({ id, value }) => ( - onSelect(id)} selected={selected === id}> - {value} - - )} - gap={0} - /> - - - - - - - ); -}; - -const FieldWrapper = styled.div` - min-height: 84px; - max-height: 224px; - border-radius: 4px; - border: 1px solid rgba(77, 83, 88, 1); - overflow: auto; -`; - -const Item = styled.div<{ selected: boolean }>` - display: flex; - align-items: center; - padding: 0 12px; - height: 28px; - cursor: pointer; - background: ${({ theme, selected }) => (selected ? theme.select.main : "inherit")}; - &:hover { - background: ${({ theme, selected }) => (selected ? theme.select.main : theme.bg[2])}; - } -`; - -const StyledText = styled(Text)` - display: -webkit-box; - -webkit-line-clamp: 1; - -webkit-box-orient: vertical; - overflow: hidden; - text-overflow: ellipsis; -`; - -const ButtonGroup = styled.div` - display: flex; - gap: 4px; -`; - -const ButtonWrapper = styled(Button)` - height: 28px; - width: 100%; - padding: 0px; - margin: 0px; - opacity: ${({ disabled }) => (disabled ? 0.6 : 1)}; -`; - -export default ListField; diff --git a/web/src/beta/components/fields/LocationField/index.stories.tsx b/web/src/beta/components/fields/LocationField/index.stories.tsx deleted file mode 100644 index 10a6a67360..0000000000 --- a/web/src/beta/components/fields/LocationField/index.stories.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Meta, StoryObj } from "@storybook/react"; - -import LocationField from "."; - -const meta: Meta = { - component: LocationField, -}; - -export default meta; - -type Story = StoryObj; - -export const Sample: Story = { - args: { - name: "Location Field", - description: "Location field description", - value: { lat: 87.43, lng: 107.53 }, - onChange: () => console.log("clicked"), - }, -}; diff --git a/web/src/beta/components/fields/LocationField/index.tsx b/web/src/beta/components/fields/LocationField/index.tsx deleted file mode 100644 index 763c121ca2..0000000000 --- a/web/src/beta/components/fields/LocationField/index.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useCallback, useState } from "react"; - -import { LatLng } from "@reearth/beta/utils/value"; -import { useT } from "@reearth/services/i18n"; -import { styled } from "@reearth/services/theme"; - -import Property from ".."; -import NumberInput from "../common/NumberInput"; - -type Props = { - name?: string; - description?: string; - value?: LatLng; - onChange?: (location: LatLng) => void; -}; - -const LocationField: React.FC = ({ name, description, value, onChange }) => { - const t = useT(); - const [location, setLocation] = useState(value || { lat: 0, lng: 0 }); - - const handleChange = useCallback( - (coordination: string, newValue: number | undefined) => { - if (newValue === undefined) return; - - setLocation(prevLocation => ({ - ...prevLocation, - [coordination === "Latitude" ? "lat" : "lng"]: newValue, - })); - onChange?.(location); - }, - [location, onChange], - ); - - return ( - - - handleChange("Latitude", newValue)} - /> - handleChange("Longitude", newValue)} - /> - - - ); -}; - -export default LocationField; - -const Wrapper = styled.div` - display: flex; - align-items: flex-start; - gap: 4px; -`; diff --git a/web/src/beta/components/fields/NumberField/index.stories.tsx b/web/src/beta/components/fields/NumberField/index.stories.tsx deleted file mode 100644 index 1bb14affd4..0000000000 --- a/web/src/beta/components/fields/NumberField/index.stories.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { Meta, StoryObj } from "@storybook/react"; - -import NumberField, { Props } from "."; - -const meta: Meta = { - component: NumberField, -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - args: { - name: "Number Field", - description: "The Number field", - value: 42, - inputDescription: "Value", - suffix: "px", - }, -}; - -export const Disabled: Story = { - args: { - name: "Number Field", - description: "The Number field", - value: 15, - inputDescription: "Disabled", - suffix: "px", - disabled: true, - }, -}; - -export const Range: Story = { - args: { - name: "Number Field", - description: "The Number field", - value: 50, - inputDescription: "Range", - suffix: "px", - min: 4, - max: 100, - }, -}; - -export const NoValue: Story = { - args: { - name: "Number Field", - description: "The Number field", - inputDescription: "No Value", - suffix: "px", - }, -}; - -export const WithMinValue: Story = { - args: { - name: "Number Field", - description: "The Number field", - value: 5, - inputDescription: "With Min Value", - suffix: "px", - min: 0, - }, -}; - -export const WithMaxValue: Story = { - args: { - name: "Number Field", - description: "The Number field", - value: 95, - inputDescription: "With Max Value", - suffix: "px", - max: 100, - }, -}; - -export const Editable: Story = { - args: { - name: "Number Field", - description: "The Number field", - value: 25, - inputDescription: "Editable", - suffix: "px", - }, -}; diff --git a/web/src/beta/components/fields/NumberField/index.tsx b/web/src/beta/components/fields/NumberField/index.tsx deleted file mode 100644 index 3f83b292dc..0000000000 --- a/web/src/beta/components/fields/NumberField/index.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import Property from ".."; -import NumberInput from "../common/NumberInput"; - -export type Props = { - name?: string; - description?: string; - suffix?: string; - min?: number; - max?: number; - disabled?: boolean; - inputDescription?: string; - value?: number; - onChange?: (value?: number | undefined) => void; -}; - -const NumberField: React.FC = ({ - name, - description, - value, - min, - max, - suffix, - inputDescription, - disabled, - onChange, -}) => { - return ( - - - - ); -}; - -export default NumberField; diff --git a/web/src/beta/components/fields/Property/PropertyField/hooks.ts b/web/src/beta/components/fields/Property/PropertyField/hooks.ts deleted file mode 100644 index abc9e321ad..0000000000 --- a/web/src/beta/components/fields/Property/PropertyField/hooks.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { useCallback } from "react"; - -import { ValueType, ValueTypes } from "@reearth/beta/utils/value"; -import { usePropertyFetcher } from "@reearth/services/api"; - -export default (propertyId: string, schemaGroup: string) => { - const { useUpdatePropertyValue, useAddPropertyItem, useRemovePropertyItem, useMovePropertyItem } = - usePropertyFetcher(); - - const handlePropertyValueUpdate = useCallback( - (fieldId: string, vt: ValueType, itemId?: string) => { - return async (v?: ValueTypes[ValueType]) => { - await useUpdatePropertyValue(propertyId, schemaGroup, itemId, fieldId, "en", v, vt); - }; - }, - [propertyId, schemaGroup, useUpdatePropertyValue], - ); - - const handleAddPropertyItem = useCallback(() => { - return useAddPropertyItem(propertyId, schemaGroup); - }, [propertyId, schemaGroup, useAddPropertyItem]); - - const handleRemovePropertyItem = useCallback( - (itemId: string) => { - return useRemovePropertyItem(propertyId, schemaGroup, itemId); - }, - [propertyId, schemaGroup, useRemovePropertyItem], - ); - - const handleMovePropertyItem = useCallback( - ({ id }: { id: string }, index: number) => { - return useMovePropertyItem(propertyId, schemaGroup, id, index); - }, - [propertyId, schemaGroup, useMovePropertyItem], - ); - - return { - handlePropertyValueUpdate, - handleAddPropertyItem, - handleRemovePropertyItem, - handleMovePropertyItem, - }; -}; diff --git a/web/src/beta/components/fields/Property/PropertyField/index.tsx b/web/src/beta/components/fields/Property/PropertyField/index.tsx deleted file mode 100644 index c3d4d5599e..0000000000 --- a/web/src/beta/components/fields/Property/PropertyField/index.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import { useMemo } from "react"; - -import { LatLng } from "@reearth/beta/utils/value"; -import { FlyTo } from "@reearth/core"; -import { Field, SchemaField } from "@reearth/services/api/propertyApi/utils"; - -import CameraField from "../../CameraField"; -import { Camera } from "../../CameraField/types"; -import ColorField from "../../ColorField"; -import DateTimeField from "../../DateTimeField"; -import LocationField from "../../LocationField"; -import NumberField from "../../NumberField"; -import RangeField from "../../RangeField"; -import SelectField from "../../SelectField"; -import SliderField from "../../SliderField"; -import SpacingInput, { SpacingValues } from "../../SpacingInput"; -import TextInput from "../../TextField"; -import ToggleField from "../../ToggleField"; -import URLField from "../../URLField"; - -import useHooks from "./hooks"; - -type Props = { - propertyId: string; - itemId?: string; - schemaGroup: string; - schema: SchemaField; - field?: Field; - onFlyTo?: FlyTo; -}; - -const PropertyField: React.FC = ({ - propertyId, - itemId, - field, - schemaGroup, - schema, - onFlyTo, -}) => { - const { handlePropertyValueUpdate } = useHooks(propertyId, schemaGroup); - - const value = useMemo( - () => field?.mergedValue ?? field?.value ?? schema.defaultValue, - [field?.mergedValue, field?.value, schema.defaultValue], - ); - - const handleChange = handlePropertyValueUpdate(schema.id, schema.type, itemId); - return ( - <> - {schema.type === "string" ? ( - schema.ui === "datetime" ? ( - - ) : schema.ui === "color" ? ( - - ) : schema.ui === "selection" || schema.choices ? ( - - ) : schema.ui === "buttons" ? ( -

Button radio field

- ) : ( - - ) - ) : schema.type === "url" ? ( - - ) : schema.type === "spacing" ? ( - - ) : schema.type === "bool" ? ( - - ) : schema.type === "number" ? ( - schema.ui === "slider" ? ( - - ) : ( - - ) - ) : schema.type === "latlng" ? ( - - ) : schema.type === "camera" ? ( - - ) : schema.type === "array" && schema.ui === "range" ? ( - - ) : ( -

{schema.name} field

- )} - - ); -}; - -export default PropertyField; diff --git a/web/src/beta/components/fields/Property/PropertyItem/index.tsx b/web/src/beta/components/fields/Property/PropertyItem/index.tsx deleted file mode 100644 index 8566a4c36a..0000000000 --- a/web/src/beta/components/fields/Property/PropertyItem/index.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { useMemo, useState } from "react"; - -import { ValueType, ValueTypes, zeroValues } from "@reearth/beta/utils/value"; -import { FlyTo } from "@reearth/core"; -import { Group, GroupListItem, Item } from "@reearth/services/api/propertyApi/utils"; -import { useT } from "@reearth/services/i18n"; - -import PropertyField from "../PropertyField"; -import PropertyList, { ListItem } from "../PropertyList"; - -type Props = { - propertyId: string; - item?: Item; - onFlyTo?: FlyTo; -}; - -const PropertyItem: React.FC = ({ propertyId, item, onFlyTo }) => { - const t = useT(); - const [selected, select] = useState(); - - const isList = item && "items" in item; - const layerMode = useMemo(() => { - if (!isList || !item?.representativeField) return false; - const sf = item.schemaFields.find(f => f.id === item.representativeField); - return sf?.type === "ref" && sf.ui === "layer"; - }, [isList, item?.representativeField, item?.schemaFields]); - - const groups = useMemo<(GroupListItem | Group)[]>( - () => (item && "items" in item ? item.items : item ? [item] : []), - [item], - ); - - const selectedItem = isList ? groups.find(g => g.id === selected) : groups[0]; - - const propertyListItems = useMemo( - () => - groups - .map(i => { - if (!i.id) return; - - const representativeField = item?.representativeField - ? i.fields.find(f => f.id === item.representativeField) - : undefined; - const nameSchemaField = item?.schemaFields?.find( - sf => sf.id === item.representativeField, - ); - - const value = representativeField?.value || nameSchemaField?.defaultValue; - - const choice = nameSchemaField?.choices - ? nameSchemaField?.choices?.find(c => c.key === value)?.label - : undefined; - - const title = valueToString(choice || value); - - return { - id: i.id, - title: (!layerMode ? title : undefined) ?? t("Settings"), - layerId: layerMode ? title : undefined, - }; - }) - .filter((g): g is ListItem => !!g), - [groups, layerMode, item, t], - ); - const schemaFields = useMemo( - () => - selectedItem - ? item?.schemaFields.map(f => { - const field = selectedItem?.fields.find(f2 => f2.id === f.id); - const condf = f.only && selectedItem?.fields.find(f2 => f2.id === f.only?.field); - const condsf = f.only && item.schemaFields.find(f2 => f2.id === f.only?.field); - const condv = - condf?.value ?? - condf?.mergedValue ?? - condsf?.defaultValue ?? - (condsf?.type ? zeroValues[condsf.type] : undefined); - return { - schemaField: f, - field, - hidden: f.only && (!condv || condv !== f.only.value), - }; - }) - : [], - [item?.schemaFields, selectedItem], - ); - - return ( - <> - {isList && !!item && ( - - )} - {!!item && - schemaFields?.map(f => { - if ((layerMode && f.schemaField.id === item.representativeField) || f.hidden) return null; - return ( - - ); - })} - - ); -}; - -export default PropertyItem; - -const valueToString = (v: ValueTypes[ValueType] | undefined): string | undefined => { - if (typeof v === "string" || typeof v === "number") { - return v.toString(); - } - return undefined; -}; diff --git a/web/src/beta/components/fields/Property/PropertyList/index.stories.tsx b/web/src/beta/components/fields/Property/PropertyList/index.stories.tsx deleted file mode 100644 index 0eeb5896e6..0000000000 --- a/web/src/beta/components/fields/Property/PropertyList/index.stories.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { useArgs } from "@storybook/preview-api"; -import { Meta, StoryObj } from "@storybook/react"; -import { useCallback } from "react"; - -import { styled } from "@reearth/services/theme"; - -import ListField, { Props } from "."; - -const meta: Meta = { - component: ListField, -}; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = (args: Props) => { - const [_, updateArgs] = useArgs(); - - const onSelect = useCallback((id: string) => updateArgs({ selected: id }), [updateArgs]); - - return ( - -
- -
-
- ); -}; - -const Wrapper = styled.div` - display: flex; - flex-direction: column; - gap: 10%; - margin-left: 2rem; - margin-top: 2rem; - height: 300px; - width: 300px; -`; - -Default.args = { - name: "List Field", - description: "List field Sample description", - items: [ - { - id: "w3tlwi", - title: "Item w3tlwi", - }, - { - id: "77eg5", - title: "Item 77eg5", - }, - { - id: "7p218", - title: "Item 7p218", - }, - { - id: "xquyo", - title: "Item xquyo", - }, - { - id: "2mewj", - title: "Item 2mewj", - }, - { - id: "d2gmu", - title: "Item d2gmu", - }, - ], -}; diff --git a/web/src/beta/components/fields/Property/PropertyList/index.tsx b/web/src/beta/components/fields/Property/PropertyList/index.tsx deleted file mode 100644 index 7e905afa1b..0000000000 --- a/web/src/beta/components/fields/Property/PropertyList/index.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import { useCallback, useEffect, useMemo } from "react"; - -import Button from "@reearth/beta/components/Button"; -import DragAndDropList from "@reearth/beta/components/DragAndDropList"; -import Property from "@reearth/beta/components/fields"; -import Text from "@reearth/beta/components/Text"; -import { useT } from "@reearth/services/i18n"; -import { styled } from "@reearth/services/theme"; - -import useHooks from "../hooks"; - -export type ListItem = { - id: string; - title: string; -}; - -export type Props = { - className?: string; - name?: string; - description?: string; - items: ListItem[]; - propertyId: string; - schemaGroup: string; - selected?: string; - atLeastOneItem?: boolean; - onSelect: (id: string) => void; -}; - -const PropertyList: React.FC = ({ - className, - name, - description, - items, - propertyId, - schemaGroup, - selected, - atLeastOneItem, - onSelect, -}: Props) => { - const t = useT(); - - const { handleAddPropertyItem, handleRemovePropertyItem, handleMovePropertyItem } = useHooks( - propertyId, - schemaGroup, - ); - - const deleteItem = useCallback(() => { - if (!selected) return; - handleRemovePropertyItem(selected); - }, [selected, handleRemovePropertyItem]); - - const getId = useCallback(({ id }: ListItem) => { - return id; - }, []); - - const disableRemoveButton = useMemo(() => { - if (!selected || (atLeastOneItem && items.length === 1)) return true; - - return !items.find(({ id }) => id == selected); - }, [items, selected, atLeastOneItem]); - - // if atleastOneItem is true, make sure one item is always selected - useEffect(() => { - if (!atLeastOneItem) return; - - const updateSelected = !selected || !items.find(({ id }) => id === selected); - if (updateSelected) { - onSelect(items[0]?.id); - } - }, [selected, items, atLeastOneItem, onSelect]); - - return ( - - - - uniqueKey="ListField" - items={items} - onItemDrop={handleMovePropertyItem} - getId={getId} - renderItem={({ id, title }) => ( - onSelect(id)} selected={selected === id}> - {title} - - )} - gap={0} - /> - - - - - - - ); -}; - -const FieldWrapper = styled.div` - min-height: 84px; - max-height: 224px; - border-radius: 4px; - border: 1px solid rgba(77, 83, 88, 1); - overflow: auto; -`; - -const Item = styled.div<{ selected: boolean }>` - display: flex; - align-items: center; - padding: 0 12px; - height: 28px; - cursor: pointer; - background: ${({ theme, selected }) => (selected ? theme.select.main : "inherit")}; - &:hover { - background: ${({ theme, selected }) => (selected ? theme.select.main : theme.bg[2])}; - } -`; - -const StyledText = styled(Text)` - display: -webkit-box; - -webkit-line-clamp: 1; - -webkit-box-orient: vertical; - overflow: hidden; - text-overflow: ellipsis; -`; - -const ButtonGroup = styled.div` - display: flex; - gap: 4px; -`; - -const ButtonWrapper = styled(Button)` - height: 28px; - width: 100%; - padding: 0px; - margin: 0px; - opacity: ${({ disabled }) => (disabled ? 0.6 : 1)}; -`; - -export default PropertyList; diff --git a/web/src/beta/components/fields/Property/hooks.ts b/web/src/beta/components/fields/Property/hooks.ts deleted file mode 100644 index abc9e321ad..0000000000 --- a/web/src/beta/components/fields/Property/hooks.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { useCallback } from "react"; - -import { ValueType, ValueTypes } from "@reearth/beta/utils/value"; -import { usePropertyFetcher } from "@reearth/services/api"; - -export default (propertyId: string, schemaGroup: string) => { - const { useUpdatePropertyValue, useAddPropertyItem, useRemovePropertyItem, useMovePropertyItem } = - usePropertyFetcher(); - - const handlePropertyValueUpdate = useCallback( - (fieldId: string, vt: ValueType, itemId?: string) => { - return async (v?: ValueTypes[ValueType]) => { - await useUpdatePropertyValue(propertyId, schemaGroup, itemId, fieldId, "en", v, vt); - }; - }, - [propertyId, schemaGroup, useUpdatePropertyValue], - ); - - const handleAddPropertyItem = useCallback(() => { - return useAddPropertyItem(propertyId, schemaGroup); - }, [propertyId, schemaGroup, useAddPropertyItem]); - - const handleRemovePropertyItem = useCallback( - (itemId: string) => { - return useRemovePropertyItem(propertyId, schemaGroup, itemId); - }, - [propertyId, schemaGroup, useRemovePropertyItem], - ); - - const handleMovePropertyItem = useCallback( - ({ id }: { id: string }, index: number) => { - return useMovePropertyItem(propertyId, schemaGroup, id, index); - }, - [propertyId, schemaGroup, useMovePropertyItem], - ); - - return { - handlePropertyValueUpdate, - handleAddPropertyItem, - handleRemovePropertyItem, - handleMovePropertyItem, - }; -}; diff --git a/web/src/beta/components/fields/RangeField/index.stories.tsx b/web/src/beta/components/fields/RangeField/index.stories.tsx deleted file mode 100644 index 0403b37065..0000000000 --- a/web/src/beta/components/fields/RangeField/index.stories.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { useArgs } from "@storybook/preview-api"; -import { Meta, StoryObj } from "@storybook/react"; -import { useCallback } from "react"; - -import { styled } from "@reearth/services/theme"; - -import RangeField, { Props } from "."; - -const meta: Meta = { - component: RangeField, -}; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = (args: Props) => { - const [_, updateArgs] = useArgs(); - - const handleChange = useCallback( - (value: number[]) => { - updateArgs({ value: value }); - }, - [updateArgs], - ); - - return ( - -
- -
-
- -
-
- ); -}; - -const Wrapper = styled.div` - display: flex; - flex-direction: column; - gap: 10%; - margin: 2rem; - height: 300px; -`; - -Default.args = { - name: "Slider Field", - description: "Slider field Sample description", - value: [0, 50], - min: 0, - max: 100, - step: 1, - disabled: false, - onChange: () => console.log("clicked"), -}; diff --git a/web/src/beta/components/fields/RangeField/index.tsx b/web/src/beta/components/fields/RangeField/index.tsx deleted file mode 100644 index 6acd490999..0000000000 --- a/web/src/beta/components/fields/RangeField/index.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { useCallback, useEffect, useState } from "react"; - -import Property from "@reearth/beta/components/fields"; -import RangeSlider, { Props as RangeProps } from "@reearth/beta/components/RangeSlider"; - -export type Props = { - name?: string; - description?: string; -} & RangeProps; - -const RangeField: React.FC = ({ name, description, value, onChange, ...args }: Props) => { - const [internalState, setInternalState] = useState(value); - - const handleChange = useCallback( - (value: number[]) => { - setInternalState(value); - }, - [setInternalState], - ); - - useEffect(() => { - setInternalState(value); - }, [value]); - - return ( - - - - ); -}; - -export default RangeField; diff --git a/web/src/beta/components/fields/SelectField/index.stories.tsx b/web/src/beta/components/fields/SelectField/index.stories.tsx deleted file mode 100644 index cfeb83b4d5..0000000000 --- a/web/src/beta/components/fields/SelectField/index.stories.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { useArgs } from "@storybook/preview-api"; -import { Meta, StoryObj } from "@storybook/react"; -import { useCallback } from "react"; - -import { styled } from "@reearth/services/theme"; - -import SelectField, { SingleSelectProps, MultiSelectProps } from "."; - -const meta: Meta = { - component: SelectField, -}; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = (args: SingleSelectProps) => { - const [_, updateArgs] = useArgs(); - - const handleChange = useCallback( - (value: string) => { - updateArgs({ value: value }); - }, - [updateArgs], - ); - - return ( - -
- -
-
- -
-
- -
-
- ); -}; - -export const MultiSelect: Story = (args: MultiSelectProps) => { - const [_, updateArgs] = useArgs(); - - const handleChange = useCallback( - (value: string[] | undefined) => { - updateArgs({ value: value }); - }, - [updateArgs], - ); - - return ( - -
- -
-
- -
-
- ); -}; - -const Wrapper = styled.div` - display: flex; - flex-direction: column; - gap: 10%; - margin-left: 2rem; - margin-right: 2rem; - margin-top: 2rem; - height: 300px; -`; - -Default.args = { - name: "Select Field", - description: "Select from the options ", - disabled: false, - value: undefined, - options: [ - { - label: "item 1 akas bakas moti kiran kapoor takhat buland biba kaur", - key: "item_1", - }, - { label: "item 2", key: "item_2" }, - { label: "item 3", key: "item_3" }, - ], - onChange: () => console.log("clicked"), -}; - -MultiSelect.args = { - name: "Select Field", - description: "Select from the options ", - disabled: false, - value: undefined, - options: [ - { - label: "item 1", - key: "item_1", - }, - { label: "item 2", key: "item_2" }, - { label: "item 3", key: "item_3" }, - { label: "item 4", key: "item_4" }, - { label: "item 5", key: "item_5" }, - { label: "item 6", key: "item_6" }, - { label: "item 7", key: "item_7" }, - ], - multiSelect: true, - onChange: () => console.log("clicked"), -}; diff --git a/web/src/beta/components/fields/SelectField/index.tsx b/web/src/beta/components/fields/SelectField/index.tsx deleted file mode 100644 index dc28280039..0000000000 --- a/web/src/beta/components/fields/SelectField/index.tsx +++ /dev/null @@ -1,258 +0,0 @@ -import React, { useState, useCallback, useMemo } from "react"; - -import Icon from "@reearth/beta/components/Icon"; -import * as Popover from "@reearth/beta/components/Popover"; -import Text from "@reearth/beta/components/Text"; -import { useT } from "@reearth/services/i18n"; -import { styled } from "@reearth/services/theme"; - -import Property from ".."; - -export type SelectValue = { - label: string; - key: string; -}; - -type CommonProps = { - className?: string; - options?: SelectValue[]; - disabled?: boolean; - // Property field - name?: string; - description?: string; - attachToRoot?: boolean; -}; - -export type SingleSelectProps = { - onChange: (key: string) => void; - value?: string; - multiSelect?: false; -} & CommonProps; - -export type MultiSelectProps = { - onChange: (keys: string[] | undefined) => void; - value?: string[]; - multiSelect: true; -} & CommonProps; - -export type Props = SingleSelectProps | MultiSelectProps; - -// TODO: Fix the onChange method TS error -const SelectField: React.FC = ({ - className, - options, - onChange, - value, - multiSelect, - disabled = false, - name, - description, - attachToRoot, -}) => { - const t = useT(); - - const [open, setOpen] = useState(false); - - const handlePopOver = useCallback(() => !disabled && setOpen(!open), [open, disabled]); - - const handleClick = useCallback( - (key: string) => { - if (multiSelect === true) { - // handle multiselect - if (value && Array.isArray(value)) { - const tempArray = [...value]; - tempArray.includes(key) - ? tempArray.splice(tempArray.indexOf(key), 1) - : tempArray.push(key); - onChange(tempArray.length > 0 ? [...tempArray] : undefined); - } else { - onChange([key]); - } - return; - } - - setOpen(false); - key != value && onChange(key); - return; - }, - [setOpen, onChange, value, multiSelect], - ); - - const selected = useMemo(() => { - return value - ? Array.isArray(value) - ? value.map(key => ({ - key, - label: options?.find(x => x.key === key)?.label, - })) - : options?.find(x => x.key === value) - : undefined; - }, [options, value]); - - const checkSelected = useCallback( - (key: string) => { - return value ? (Array.isArray(value) ? value.includes(key) : value === key) : false; - }, - [value], - ); - - return ( - - - 0}> - - - - - - - - {options?.map(({ label: value, key }) => ( - handleClick(key)} - selected={checkSelected(key)}> - {multiSelect && ( - - )} - {value} - - ))} - - {Array.isArray(selected) && ( - - {selected.map(({ label, key }) => ( - - - {label} - - !disabled && handleClick(key)} - disabled={disabled} - /> - - ))} - - )} - - - - ); -}; - -const ProviderWrapper = styled.div<{ multiSelect: boolean }>` - display: flex; - flex-direction: column; - gap: 6px; - border-radius: 4px; - width: 100%; -`; - -const InputWrapper = styled.div<{ disabled: boolean }>` - display: flex; - position: relative; - opacity: ${({ disabled }) => (disabled ? 0.6 : 1)}; - cursor: ${({ disabled }) => (disabled ? "not-allowed" : "pointer")}; -`; - -const Select = styled.div<{ open: boolean; selected: boolean }>` - padding: 8px; - /* The width + placement of the arrow icon */ - padding-right: 22px; - border-radius: 4px; - width: 100%; - font-size: 12px; - line-height: 1; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - color: ${({ theme, selected }) => (selected ? theme.content.main : theme.content.weaker)}; - background: ${({ theme }) => theme.bg[1]}; - box-shadow: ${({ theme }) => theme.shadow.input}; - border: ${({ theme, open }) => - open ? `1px solid ${theme.select.strong}` : `1px solid ${theme.outline.weak}`}; - - &:focus { - border: 1px solid ${({ theme }) => theme.select.strong}; - } - - &:focus-visible { - border: 1px solid ${({ theme }) => theme.select.strong}; - outline: none; - } -`; - -const ArrowIcon = styled(Icon)<{ open: boolean }>` - position: absolute; - right: 10px; - top: 50%; - transform: ${({ open }) => (open ? "translateY(-50%) scaleY(-1)" : "translateY(-50%)")}; - color: ${({ theme }) => theme.content.main}; -`; - -const PickerWrapper = styled(Popover.Content)` - min-width: 150px; - border: 1px solid ${({ theme }) => theme.outline.weak}; - outline: none; - border-radius: 4px; - background: ${({ theme }) => theme.bg[1]}; - box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.25); - display: flex; - flex-direction: column; - justify-content: space-between; - max-height: 200px; - overflow-y: auto; -`; - -const OptionWrapper = styled.div<{ selected: boolean }>` - display: flex; - padding: 4px 8px; - align-items: center; - cursor: "pointer"; - background: ${({ theme, selected }) => (selected ? theme.bg[2] : "inherit")}; - - &:hover { - background: ${({ theme, selected }) => (selected ? theme.bg[2] : theme.select.main)}; - } -`; - -const CheckIcon = styled(Icon)` - margin-right: 12px; -`; - -const SelectedWrapper = styled.div` - display: flex; - flex-direction: column; - gap: 2px; - max-height: 125px; - overflow: auto; -`; - -const Selected = styled.div<{ disabled: boolean }>` - display: flex; - padding: 4px 8px; - align-items: center; - justify-content: space-between; - border-radius: 4px; - background: ${({ theme }) => theme.bg[2]}; - opacity: ${({ disabled }) => (disabled ? 0.6 : 1)}; -`; - -const CancelIcon = styled(Icon)<{ disabled: boolean }>` - cursor: ${({ disabled }) => (disabled ? "not-allowed" : "pointer")}; - color: ${({ theme }) => theme.content.weak}; - - :hover { - color: ${({ theme }) => theme.content.main}; - } -`; - -export default SelectField; diff --git a/web/src/beta/components/fields/SliderField/index.stories.tsx b/web/src/beta/components/fields/SliderField/index.stories.tsx deleted file mode 100644 index 249a2cd543..0000000000 --- a/web/src/beta/components/fields/SliderField/index.stories.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { useArgs } from "@storybook/preview-api"; -import { Meta, StoryObj } from "@storybook/react"; -import { useCallback } from "react"; - -import { styled } from "@reearth/services/theme"; - -import SliderField, { Props } from "."; - -const meta: Meta = { - component: SliderField, -}; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = (args: Props) => { - const [_, updateArgs] = useArgs(); - - const handleChange = useCallback( - (value: number) => { - updateArgs({ value: value }); - }, - [updateArgs], - ); - - return ( - -
- -
-
- -
-
- -
-
- ); -}; - -const Wrapper = styled.div` - display: flex; - flex-direction: column; - gap: 10%; - margin: 2rem; - height: 300px; -`; - -Default.args = { - name: "Slider Field", - description: "Slider field Sample description", - value: 50, - min: 0, - max: 100, - step: 1, - disabled: false, - onChange: () => console.log("clicked"), -}; diff --git a/web/src/beta/components/fields/SliderField/index.tsx b/web/src/beta/components/fields/SliderField/index.tsx deleted file mode 100644 index 18689c83c2..0000000000 --- a/web/src/beta/components/fields/SliderField/index.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { useCallback, useEffect, useState } from "react"; - -import Property from "@reearth/beta/components/fields"; -import Slider, { Props as SliderProps } from "@reearth/beta/components/Slider"; - -export type Props = { - name?: string; - description?: string; -} & SliderProps; - -const SliderField: React.FC = ({ name, description, value, onChange, ...args }: Props) => { - const [internalState, setInternalState] = useState(value); - - const handleChange = useCallback( - (value: number) => { - setInternalState(value); - }, - [setInternalState], - ); - - useEffect(() => { - setInternalState(value); - }, [value]); - - return ( - - - - ); -}; - -export default SliderField; diff --git a/web/src/beta/components/fields/SpacingInput/index.stories.tsx b/web/src/beta/components/fields/SpacingInput/index.stories.tsx deleted file mode 100644 index a5ae5dc7e4..0000000000 --- a/web/src/beta/components/fields/SpacingInput/index.stories.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Meta, Story } from "@storybook/react"; - -import SpacingInput from "./index"; - -export default { - component: SpacingInput, -} as Meta; - -const Template: Story = args => ; - -export const Default = Template.bind({}); -Default.args = { - name: "Padding", - description: "Adjust the padding values", -}; - -export const WithValues = Template.bind({}); -WithValues.args = { - name: "Padding", - description: "Adjust the padding values", - value: { - top: "10", - left: "20", - right: "30", - bottom: "40", - }, -}; diff --git a/web/src/beta/components/fields/SpacingInput/index.tsx b/web/src/beta/components/fields/SpacingInput/index.tsx deleted file mode 100644 index 05b18db798..0000000000 --- a/web/src/beta/components/fields/SpacingInput/index.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { useMemo, useState } from "react"; - -import { styled } from "@reearth/services/theme"; - -import Property from ".."; -import NumberInput from "../common/NumberInput"; - -export type SpacingValues = { - top: number; - left: number; - right: number; - bottom: number; -}; - -const SPACING_POSITIONS = ["top", "left", "right", "bottom"]; - -type Props = { - name?: string; - description?: string; - value?: SpacingValues; - min?: number; - max?: number; - onChange?: (values: SpacingValues) => void; -}; - -type Position = keyof SpacingValues; - -const SpacingInput: React.FC = ({ name, description, value, min, max, onChange }) => { - const [spacingValues, setSpacingValues] = useState( - value || { top: 0, left: 0, right: 0, bottom: 0 }, - ); - - const memoizedSpacingValues = useMemo( - () => - SPACING_POSITIONS.map(position => getSpacingPosition(spacingValues, position as Position)), - [spacingValues], - ); - - const handleInputChange = (position: Position, newValue?: number) => { - const updatedValues = { ...spacingValues, [position]: newValue }; - setSpacingValues(updatedValues); - onChange?.(updatedValues); - }; - - return ( - - - {SPACING_POSITIONS.map((position, index) => ( - handleInputChange(position as Position, newValue)} - /> - ))} - - - ); -}; - -export default SpacingInput; - -const StyledRectangle = styled.div` - display: flex; - width: 100%; - height: 94px; - border: 1px dashed ${({ theme }) => theme.outline.weak}; - box-sizing: border-box; - position: relative; -`; - -const SpacingField = styled(NumberInput)<{ position: string }>` - position: absolute; - ${({ position }) => - position === "top" - ? "top: 0; left: 50%; transform: translateX(-50%);" - : position === "left" - ? "left: 0; top: 50%; transform: translateY(-50%);" - : position === "right" - ? "right: 0; top: 50%; transform: translateY(-50%);" - : "bottom: 0; left: 50%; transform: translateX(-50%);"}; -`; - -function getSpacingPosition(spacingValue: SpacingValues, position: Position): number { - return spacingValue[position]; -} diff --git a/web/src/beta/components/fields/TextAreaField/index.stories.tsx b/web/src/beta/components/fields/TextAreaField/index.stories.tsx deleted file mode 100644 index e7ead5521e..0000000000 --- a/web/src/beta/components/fields/TextAreaField/index.stories.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { useArgs } from "@storybook/preview-api"; -import { Meta, StoryObj } from "@storybook/react"; -import { useCallback } from "react"; - -import { styled } from "@reearth/services/theme"; - -import TextAreaField, { Props } from "."; - -const meta: Meta = { - component: TextAreaField, -}; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = (args: Props) => { - const [_, updateArgs] = useArgs(); - - const handleChange = useCallback((value: string) => updateArgs({ value: value }), [updateArgs]); - - return ( - -
- -
-
- -
-
- -
-
- ); -}; - -const Wrapper = styled.div` - display: flex; - flex-direction: column; - gap: 10%; - margin-left: 2rem; - margin-right: 2rem; - margin-top: 2rem; - height: 300px; -`; - -Default.args = { - name: "Text Area Field", - description: "Description around", - disabled: false, - value: undefined, - onChange: () => console.log("clicked"), -}; diff --git a/web/src/beta/components/fields/TextAreaField/index.tsx b/web/src/beta/components/fields/TextAreaField/index.tsx deleted file mode 100644 index 696137d8fc..0000000000 --- a/web/src/beta/components/fields/TextAreaField/index.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { debounce } from "lodash-es"; -import { useCallback, useLayoutEffect, useRef, useMemo, useState, useEffect } from "react"; - -import { useT } from "@reearth/services/i18n"; -import { styled } from "@reearth/services/theme"; - -import Property from ".."; - -export type Props = { - value: string; - onChange: (text: string) => void; - minHeight?: number; - placeholder?: string; - name?: string; - description?: string; - disabled?: boolean; - debounceBy?: number; -}; - -const TextAreaField: React.FC = ({ - name, - description, - value, - onChange, - placeholder, - minHeight = 0, - disabled, - debounceBy = 1000, -}) => { - const textareaRef = useRef(null); - const t = useT(); - - const [text, setText] = useState(value); - - const debouncedHandleTextUpdate = useMemo( - () => debounce(onChange, debounceBy), - [onChange, debounceBy], - ); - - const handleChange = useCallback( - (e: React.ChangeEvent) => { - setText(e.currentTarget.value); - debouncedHandleTextUpdate(e.currentTarget.value); - }, - [debouncedHandleTextUpdate], - ); - - useLayoutEffect(() => { - if (!textareaRef?.current) return; - // Reset height - important to shrink on delete - textareaRef.current.style.height = "inherit"; - // Set height - textareaRef.current.style.height = `${Math.max(minHeight, textareaRef.current.scrollHeight)}px`; - }, [text, minHeight]); - - useEffect(() => { - setText(value); - }, [value]); - - return ( - - - - ); -}; - -const StyledTextArea = styled.textarea<{ minHeight: number; disabled: boolean }>` - padding: 4px 8px; - width: 100%; - resize: none; - overflow: hidden; - min-height: ${({ minHeight }) => minHeight}; - border-radius: 4px; - border: ${({ theme }) => `1px solid ${theme.outline.weak}`}; - font-size: 14px; - outline: none; - color: ${({ theme }) => theme.content.main}; - background: inherit; - opacity: ${({ disabled }) => (disabled ? 0.6 : 1)}; - pointer-events: ${({ disabled }) => (disabled ? "not-allowed" : "inherit")}; - box-sizing: border-box; -`; - -export default TextAreaField; diff --git a/web/src/beta/components/fields/TextField/index.stories.tsx b/web/src/beta/components/fields/TextField/index.stories.tsx deleted file mode 100644 index e8a4d41646..0000000000 --- a/web/src/beta/components/fields/TextField/index.stories.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Meta, StoryObj } from "@storybook/react"; - -import TextField, { Props } from "./index"; - -const meta: Meta = { - component: TextField, -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - args: { - name: "Text input field", - value: "Text field", - description: "The text input description", - }, -}; - -export const WithPlaceholder: Story = { - args: { - placeholder: "Text Input", - name: "Text input field", - description: "The text input description", - }, -}; diff --git a/web/src/beta/components/fields/TextField/index.tsx b/web/src/beta/components/fields/TextField/index.tsx deleted file mode 100644 index 2d12f1864c..0000000000 --- a/web/src/beta/components/fields/TextField/index.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import TextInput from "@reearth/beta/components/fields/common/TextInput"; - -import Property from ".."; - -export type Props = { - className?: string; - name?: string; - description?: string; - value?: string; - placeholder?: string; - disabled?: boolean; - onChange?: (text: string) => void; - onBlur?: () => void; -}; - -const TextField: React.FC = ({ - className, - name, - description, - value, - placeholder, - disabled, - onChange, - onBlur, -}) => { - return ( - - - - ); -}; - -export default TextField; diff --git a/web/src/beta/components/fields/TimelineField/EditPanel/hooks.ts b/web/src/beta/components/fields/TimelineField/EditPanel/hooks.ts deleted file mode 100644 index dccb75d297..0000000000 --- a/web/src/beta/components/fields/TimelineField/EditPanel/hooks.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { useCallback, useMemo, useState } from "react"; - -import { TimelineFieldProp } from ".."; - -type Props = { - timelineValues?: TimelineFieldProp; - onChange?: (value?: TimelineFieldProp) => void; - onClose?: () => void; - setTimelineValues?: (value?: TimelineFieldProp) => void; -}; - -export default ({ timelineValues, onChange, onClose, setTimelineValues }: Props) => { - const [warning, setWarning] = useState(false); - - const handleOnChange = useCallback( - (newValue: string, fieldId: string) => { - const updatedData: TimelineFieldProp = { - ...timelineValues, - currentTime: timelineValues?.currentTime || "", - startTime: timelineValues?.startTime || "", - endTime: timelineValues?.endTime || "", - }; - - switch (fieldId) { - case "startTime": - updatedData.startTime = newValue || ""; - break; - case "currentTime": - updatedData.currentTime = newValue; - if ( - updatedData.startTime && - new Date(updatedData.currentTime.substring(0, 19)) < - new Date(updatedData.startTime.substring(0, 19)) - ) { - setWarning(true); - } else { - setWarning(false); - } - break; - case "endTime": - updatedData.endTime = newValue; - if ( - updatedData.endTime && - new Date(updatedData.currentTime.substring(0, 19)) > - new Date(updatedData?.endTime?.substring(0, 19)) - ) { - setWarning(true); - } else { - setWarning(false); - } - break; - default: - break; - } - - setTimelineValues?.(updatedData); - }, - [timelineValues, setTimelineValues], - ); - - const [disabledFields, setDisabledFields] = useState([]); - - const handlePopoverOpen = useCallback((fieldId?: string) => { - switch (fieldId) { - case "startTime": - setDisabledFields(["endTime", "currentTime"]); - break; - case "endTime": - setDisabledFields(["startTime", "currentTime"]); - break; - case "currentTime": - setDisabledFields(["startTime", "endTime"]); - break; - default: - setDisabledFields([]); - break; - } - }, []); - - const handleApplyChange = useCallback(() => { - if ( - timelineValues?.currentTime !== "" && - timelineValues?.startTime !== "" && - timelineValues?.endTime !== "" - ) { - onChange?.(timelineValues); - onClose?.(); - } - }, [timelineValues, onChange, onClose]); - - const isDisabled = useMemo(() => { - if (timelineValues) { - return Object.values(timelineValues).every(value => value !== ""); - } - return false; - }, [timelineValues]); - - return { - warning, - isDisabled, - disabledFields, - setDisabledFields, - handleOnChange, - handlePopoverOpen, - onAppyChange: handleApplyChange, - }; -}; diff --git a/web/src/beta/components/fields/TimelineField/EditPanel/index.tsx b/web/src/beta/components/fields/TimelineField/EditPanel/index.tsx deleted file mode 100644 index 02df4370a6..0000000000 --- a/web/src/beta/components/fields/TimelineField/EditPanel/index.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { useMemo } from "react"; - -import Button from "@reearth/beta/components/Button"; -import Icon from "@reearth/beta/components/Icon"; -import Modal from "@reearth/beta/components/Modal"; -import { getTimeZone } from "@reearth/beta/features/Visualizer/Crust/StoryPanel/utils"; -import { useT } from "@reearth/services/i18n"; -import { styled } from "@reearth/services/theme"; - -import { TimelineFieldProp } from ".."; -import DateTimeField from "../../DateTimeField"; - -import useHooks from "./hooks"; - -type EditPanelProps = { - isVisible?: boolean; - timelineValues?: TimelineFieldProp; - onClose?: () => void; - onChange?: (value?: TimelineFieldProp) => void; - setTimelineValues?: (value?: TimelineFieldProp) => void; -}; - -const EditPanel = ({ - isVisible, - timelineValues, - setTimelineValues, - onClose, - onChange, -}: EditPanelProps) => { - const t = useT(); - const { - isDisabled, - warning, - disabledFields, - setDisabledFields, - handleOnChange, - onAppyChange, - handlePopoverOpen, - } = useHooks({ - timelineValues, - onChange, - onClose, - setTimelineValues, - }); - - const timezoneMatches = useMemo(() => { - if (!timelineValues) return false; - - const startTimezone = getTimeZone(timelineValues?.startTime); - const currentTimezone = getTimeZone(timelineValues?.currentTime); - const endTimezone = getTimeZone(timelineValues?.endTime); - - const areTimezonesEqual = startTimezone === currentTimezone && currentTimezone === endTimezone; - - return areTimezonesEqual; - }, [timelineValues]); - - return ( - } - button2={ -