diff --git a/.github/reviewer-lottery.yml b/.github/reviewer-lottery.yml index 7f5117cd44..e6aacd4b7c 100644 --- a/.github/reviewer-lottery.yml +++ b/.github/reviewer-lottery.yml @@ -15,5 +15,4 @@ groups: usernames: - airslice - mkumbobeaty - - pyshx - m-abe-dev diff --git a/.github/workflows/deploy_server_nightly.yml b/.github/workflows/deploy_server_nightly.yml index c16a2e6031..8c4e7d6697 100644 --- a/.github/workflows/deploy_server_nightly.yml +++ b/.github/workflows/deploy_server_nightly.yml @@ -6,20 +6,17 @@ concurrency: group: ${{ github.workflow }} cancel-in-progress: true env: + GCP_REGION: asia-northeast1 IMAGE: reearth/reearth-visualizer-api:nightly IMAGE_GCP: us-central1-docker.pkg.dev/reearth-oss/reearth/reearth-visualizer-api:nightly - GCP_REGION: asia-northeast1 jobs: deploy_test: - name: Deploy app to test env runs-on: ubuntu-latest if: github.event.repository.full_name == 'reearth/reearth-visualizer' steps: - uses: google-github-actions/auth@v0 with: credentials_json: ${{ secrets.GCP_SA_KEY }} - - name: Set up Cloud SDK - uses: google-github-actions/setup-gcloud@v0 - name: Configure docker run: gcloud auth configure-docker us-central1-docker.pkg.dev --quiet - name: docker push diff --git a/.github/workflows/deploy_web_nightly.yml b/.github/workflows/deploy_web_nightly.yml index 8c2b4d2281..4fe89b61ee 100644 --- a/.github/workflows/deploy_web_nightly.yml +++ b/.github/workflows/deploy_web_nightly.yml @@ -2,35 +2,14 @@ name: deploy-web-nightly on: workflow_dispatch: workflow_call: - +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: true env: - REEARTH_URL: visualizer.test.reearth.dev GCP_REGION: us-central1 - GCS_DEST: gs://reearth-visualizer-oss-static-bucket/ - IMAGE: reearth/reearth-visualizer-web:nightly IMAGE_GCP: us-central1-docker.pkg.dev/reearth-oss/reearth/reearth-visualizer-web:nightly - jobs: - # TODO: Remove later after migrating to Cloud Run - deploy_test_old: - runs-on: ubuntu-latest - if: github.event.repository.full_name == 'reearth/reearth-visualizer' - steps: - - uses: google-github-actions/auth@v0 - with: - credentials_json: ${{ secrets.GCP_SA_KEY }} - - name: Set up Cloud SDK - uses: google-github-actions/setup-gcloud@v0 - - uses: dsaltares/fetch-gh-release-asset@master - with: - version: tags/nightly - file: reearth-web_nightly.tar.gz - token: ${{ secrets.GITHUB_TOKEN }} - - run: tar -xvf reearth-web_nightly.tar.gz - - name: rsync - run: gsutil -m -h "Cache-Control:no-store" rsync -x "^reearth_config\\.json$|^cesium_ion_token\\.txt$" -dr reearth-web/ $GCS_DEST - deploy_test: runs-on: ubuntu-latest if: github.event.repository.full_name == 'reearth/reearth-visualizer' @@ -38,8 +17,6 @@ jobs: - uses: google-github-actions/auth@v0 with: credentials_json: ${{ secrets.GCP_SA_KEY }} - - name: Set up Cloud SDK - uses: google-github-actions/setup-gcloud@v0 - name: Configure docker run: gcloud auth configure-docker us-central1-docker.pkg.dev --quiet - name: docker push diff --git a/server/e2e/common.go b/server/e2e/common.go index bcf2e893f7..b5abd82292 100644 --- a/server/e2e/common.go +++ b/server/e2e/common.go @@ -1,8 +1,11 @@ package e2e import ( + "bytes" "context" "encoding/json" + "fmt" + "io" "net" "net/http" "regexp" @@ -244,25 +247,46 @@ func RequestWithMultipart(e *httpexpect.Expect, user string, requestBody map[str JSON() } -func JSONEqRegexp(t *testing.T, rx string, str string) bool { +func JSONEqRegexp(t *testing.T, actual string, expected string) bool { + return assert.Regexp( + t, + regexp.MustCompile(strings.ReplaceAll(aligningJSON(t, expected), "[", "\\[")), + aligningJSON(t, actual), + ) +} - var rx1 map[string]interface{} - err := json.Unmarshal([]byte(rx), &rx1) - assert.Nil(t, err) +func RegexpJSONEReadCloser(t *testing.T, actual io.ReadCloser, expected string) bool { + defer func() { + if err := actual.Close(); err != nil { + t.Errorf("failed to close reader: %v", err) + } + }() + actualBuf := new(bytes.Buffer) + _, err := actualBuf.ReadFrom(actual) + assert.NoError(t, err) + return JSONEqRegexp(t, actualBuf.String(), expected) +} - rx2, err := json.Marshal(rx1) +func JSONEqRegexpInterface(t *testing.T, actual interface{}, expected string) bool { + actualBytes, err := json.Marshal(actual) assert.Nil(t, err) + return JSONEqRegexp(t, string(actualBytes), expected) +} - var str1 map[string]interface{} - err = json.Unmarshal([]byte(str), &str1) +func aligningJSON(t *testing.T, str string) string { + // Unmarshal and Marshal to make the JSON format consistent + var obj interface{} + err := json.Unmarshal([]byte(str), &obj) assert.Nil(t, err) - - str2, err := json.Marshal(str1) + strBytes, err := json.Marshal(obj) assert.Nil(t, err) + return string(strBytes) +} - return assert.Regexp( - t, - regexp.MustCompile(strings.ReplaceAll(string(rx2), "[", "\\[")), - string(str2), - ) +func toJSONString(v interface{}) (string, error) { + jsonData, err := json.Marshal(v) + if err != nil { + return "", fmt.Errorf("failed to marshal JSON: %w", err) + } + return string(jsonData), nil } diff --git a/server/e2e/gql_asset_test.go b/server/e2e/gql_asset_test.go index d00c0c1994..9ec07b8a2b 100644 --- a/server/e2e/gql_asset_test.go +++ b/server/e2e/gql_asset_test.go @@ -129,12 +129,14 @@ func createAsset(t *testing.T, e *httpexpect.Expect, filePath string, coreSuppor }, "query": CreateAssetMutation, } + operations, err := toJSONString(requestBody) + assert.Nil(t, err) return e.POST("/api/graphql"). WithHeader("Origin", "https://example.com"). WithHeader("authorization", "Bearer test"). WithHeader("X-Reearth-Debug-User", uID.String()). WithMultipart(). - WithFormField("operations", toJSONString(requestBody)). + WithFormField("operations", operations). WithFormField("map", `{"0": ["variables.file"]}`). WithFile("0", filePath). Expect(). diff --git a/server/e2e/gql_custom_property_test.go b/server/e2e/gql_custom_property_test.go new file mode 100644 index 0000000000..f4e99769a7 --- /dev/null +++ b/server/e2e/gql_custom_property_test.go @@ -0,0 +1,411 @@ +package e2e + +import ( + "math/rand" + "testing" + "time" + + "github.com/gavv/httpexpect/v2" +) + +func TestUpdateCustomProperties(t *testing.T) { + e := Server(t, baseSeeder) + pId := createProject(e, "test") + _, _, sId := createScene(e, pId) + lId := addTestNLSLayerSimple(e, sId) + + proId1 := RandomString(10) + fId1 := addTestGeoJSONFeature(e, lId, proId1) + updateTestGeoJSONFeature(e, lId, fId1, proId1) + + proId2 := RandomString(10) + fId2 := addTestGeoJSONFeature(e, lId, proId2) + updateTestGeoJSONFeature(e, lId, fId2, proId2) + + // check GetScene + res := getNewLayersOfScene(e, sId) + JSONEqRegexpInterface(t, res.Path("$.sketch.customPropertySchema").Raw(), ` +{ + "AAA": "Text_1", + "BBB": "Int_2", + "XXX": "URL_3", + "YYY": "Boolean_4" +}`) + JSONEqRegexpInterface(t, res.Path("$.sketch.featureCollection").Raw(), ` +{ + "__typename": "FeatureCollection", + "features": [ + { + "__typename": "Feature", + "geometry": { + "__typename": "Point", + "pointCoordinates": [ + 139.75315007107542, + 35.68233694131118 + ], + "type": "Point" + }, + "id": ".*", + "properties": { + "AAA": "aaa", + "BBB": 123, + "XXX": "xxx", + "YYY": true, + "extrudedHeight": 0, + "id": ".*", + "positions": [ + [ + -3958794.0678944187, + 3350992.944211698, + 3699619.2576891473 + ] + ], + "type": "marker" + }, + "type": "Feature" + }, + { + "__typename": "Feature", + "geometry": { + "__typename": "Point", + "pointCoordinates": [ + 139.75315007107542, + 35.68233694131118 + ], + "type": "Point" + }, + "id": ".*", + "properties": { + "AAA": "aaa", + "BBB": 123, + "XXX": "xxx", + "YYY": true, + "extrudedHeight": 0, + "id": ".*", + "positions": [ + [ + -3958794.0678944187, + 3350992.944211698, + 3699619.2576891473 + ] + ], + "type": "marker" + }, + "type": "Feature" + } + ], + "type": "FeatureCollection" +}`) +} + +func TestChangeCustomPropertyTitle(t *testing.T) { + e := Server(t, baseSeeder) + sId, lId, _, _ := setupTestData(e) + // change XXX -> ZZZ + requestBody := GraphQLRequest{ + OperationName: "ChangeCustomPropertyTitle", + Query: ` + mutation ChangeCustomPropertyTitle($input: ChangeCustomPropertyTitleInput!) { + changeCustomPropertyTitle(input: $input) { + layer { + id + } + } + } + `, + Variables: map[string]any{ + "input": map[string]interface{}{ + "layerId": lId, + "schema": map[string]any{ + "AAA": "Text_1", + "BBB": "Int_2", + "ZZZ": "URL_3", // XXX -> ZZZ + "YYY": "Boolean_4", + }, + "oldTitle": "XXX", // XXX -> ZZZ + "newTitle": "ZZZ", // XXX -> ZZZ + }, + }, + } + Request(e, uID.String(), requestBody) + + // check GetScene + res := getNewLayersOfScene(e, sId) + JSONEqRegexpInterface(t, res.Path("$.sketch.customPropertySchema").Raw(), ` +{ + "AAA": "Text_1", + "BBB": "Int_2", + "ZZZ": "URL_3", + "YYY": "Boolean_4" +}`) + + features := res.Path("$.sketch.featureCollection.features").Array() + features.Length().Equal(2) + feature0 := features.Element(0).Object() + feature1 := features.Element(1).Object() + + JSONEqRegexpInterface(t, feature0.Raw(), ` +{ + "AAA": "aaa", + "BBB": 123, + "ZZZ": "xxx", + "YYY": true, + "extrudedHeight": 0, + "id": ".*", + "positions": [ + [ + -3958794.0678944187, + 3350992.944211698, + 3699619.2576891473 + ] + ], + "type": "marker" +}`) + JSONEqRegexpInterface(t, feature1.Raw(), ` +{ + "AAA": "aaa", + "BBB": 123, + "ZZZ": "xxx", + "YYY": true, + "extrudedHeight": 0, + "id": ".*", + "positions": [ + [ + -3958794.0678944187, + 3350992.944211698, + 3699619.2576891473 + ] + ], + "type": "marker" +}`) +} + +func TestRemoveCustomProperty(t *testing.T) { + e := Server(t, baseSeeder) + sId, lId, _, _ := setupTestData(e) + // remove XXX + requestBody := GraphQLRequest{ + OperationName: "RemoveCustomProperty", + Query: ` + mutation RemoveCustomProperty($input: RemoveCustomPropertyInput!) { + removeCustomProperty(input: $input) { + layer { + id + } + } + } + `, + Variables: map[string]any{ + "input": map[string]interface{}{ + "layerId": lId, + "schema": map[string]any{ + "AAA": "Text_1", + "BBB": "Int_2", + // "XXX": "URL_3", remove + "YYY": "Boolean_3", + }, + "removedTitle": "XXX", + }, + }, + } + Request(e, uID.String(), requestBody) + + // check GetScene + res := getNewLayersOfScene(e, sId) + JSONEqRegexpInterface(t, res.Path("$.sketch.customPropertySchema").Raw(), ` +{ + "AAA": "Text_1", + "BBB": "Int_2", + "YYY": "Boolean_3" +}`) + + features := res.Path("$.sketch.featureCollection.features").Array() + features.Length().Equal(2) + feature0 := features.Element(0).Object() + feature1 := features.Element(1).Object() + + JSONEqRegexpInterface(t, feature0.Raw(), ` + + { + "AAA": "aaa", + "BBB": 123, + "YYY": true, + "extrudedHeight": 0, + "id": ".*", + "positions": [ + [ + -3958794.0678944187, + 3350992.944211698, + 3699619.2576891473 + ] + ], + "type": "marker" + }`) + + JSONEqRegexpInterface(t, feature1.Raw(), ` + + { + "AAA": "aaa", + "BBB": 123, + "YYY": true, + "extrudedHeight": 0, + "id": ".*", + "positions": [ + [ + -3958794.0678944187, + 3350992.944211698, + 3699619.2576891473 + ] + ], + "type": "marker" + }`) +} + +// below Common functions ----------------------------------------------------- + +func setupTestData(e *httpexpect.Expect) (sId string, lId string, fId1 string, fId2 string) { + pId := createProject(e, "test") + _, _, sId = createScene(e, pId) + lId = addTestNLSLayerSimple(e, sId) + + proId1 := RandomString(10) + fId1 = addTestGeoJSONFeature(e, lId, proId1) + updateTestGeoJSONFeature(e, lId, fId1, proId1) + + proId2 := RandomString(10) + fId2 = addTestGeoJSONFeature(e, lId, proId2) + updateTestGeoJSONFeature(e, lId, fId2, proId2) + return +} + +func getNewLayersOfScene(e *httpexpect.Expect, sId string) *httpexpect.Object { + requestBody := GraphQLRequest{ + OperationName: "GetScene", + Query: "query GetScene($sceneId: ID!, $lang: Lang) { node(id: $sceneId, type: SCENE) { id ... on Scene { rootLayerId teamId projectId property { id ...PropertyFragment __typename } clusters { id name propertyId property { id ...PropertyFragment __typename } __typename } tags { id label ... on TagGroup { tags { id label __typename } __typename } __typename } plugins { property { id ...PropertyFragment __typename } plugin { ...PluginFragment __typename } __typename } widgets { id enabled extended pluginId extensionId property { id ...PropertyFragment __typename } __typename } widgetAlignSystem { ...WidgetAlignSystemFragment __typename } stories { ...StoryFragment __typename } newLayers { ...NLSLayerCommon __typename } styles { ...NLSLayerStyle __typename } __typename } __typename }}fragment PropertyFieldLink on PropertyFieldLink { datasetId datasetSchemaId datasetSchemaFieldId __typename}fragment PropertyFieldFragment on PropertyField { id fieldId type value links { ...PropertyFieldLink __typename } __typename}fragment PropertyGroupFragment on PropertyGroup { id schemaGroupId fields { ...PropertyFieldFragment __typename } __typename}fragment PropertyItemFragment on PropertyItem { ... on PropertyGroupList { id schemaGroupId groups { ...PropertyGroupFragment __typename } __typename } ... on PropertyGroup { ...PropertyGroupFragment __typename } __typename}fragment PropertyFragmentWithoutSchema on Property { id items { ...PropertyItemFragment __typename } __typename}fragment PropertySchemaFieldFragment on PropertySchemaField { fieldId title description placeholder translatedTitle(lang: $lang) translatedDescription(lang: $lang) translatedPlaceholder(lang: $lang) prefix suffix type defaultValue ui min max choices { key icon title translatedTitle(lang: $lang) __typename } isAvailableIf { fieldId type value __typename } __typename}fragment PropertySchemaGroupFragment on PropertySchemaGroup { schemaGroupId title collection translatedTitle(lang: $lang) isList representativeFieldId isAvailableIf { fieldId type value __typename } fields { ...PropertySchemaFieldFragment __typename } __typename}fragment WidgetAreaFragment on WidgetArea { widgetIds align padding { top bottom left right __typename } gap centered background __typename}fragment WidgetSectionFragment on WidgetSection { top { ...WidgetAreaFragment __typename } middle { ...WidgetAreaFragment __typename } bottom { ...WidgetAreaFragment __typename } __typename}fragment WidgetZoneFragment on WidgetZone { left { ...WidgetSectionFragment __typename } center { ...WidgetSectionFragment __typename } right { ...WidgetSectionFragment __typename } __typename}fragment PropertyFragment on Property { id ...PropertyFragmentWithoutSchema schema { id groups { ...PropertySchemaGroupFragment __typename } __typename } __typename}fragment StoryPageFragment on StoryPage { id title swipeable propertyId property { id ...PropertyFragment __typename } layersIds blocks { id pluginId extensionId property { id ...PropertyFragment __typename } __typename } __typename}fragment FeatureFragment on Feature { id type properties geometry { ... on Point { type pointCoordinates __typename } ... on LineString { type lineStringCoordinates __typename } ... on Polygon { type polygonCoordinates __typename } ... on MultiPolygon { type multiPolygonCoordinates __typename } ... on GeometryCollection { type geometries { ... on Point { type pointCoordinates __typename } ... on LineString { type lineStringCoordinates __typename } ... on Polygon { type polygonCoordinates __typename } ... on MultiPolygon { type multiPolygonCoordinates __typename } __typename } __typename } __typename } __typename}fragment PluginFragment on Plugin { id name extensions { extensionId description name translatedDescription(lang: $lang) translatedName(lang: $lang) icon singleOnly type widgetLayout { extendable { vertically horizontally __typename } extended floating defaultLocation { zone section area __typename } __typename } __typename } __typename}fragment WidgetAlignSystemFragment on WidgetAlignSystem { outer { ...WidgetZoneFragment __typename } inner { ...WidgetZoneFragment __typename } __typename}fragment StoryFragment on Story { id title panelPosition bgColor isBasicAuthActive basicAuthUsername basicAuthPassword alias publicTitle publicDescription publishmentStatus publicImage publicNoIndex pages { ...StoryPageFragment __typename } __typename}fragment NLSLayerCommon on NLSLayer { id index layerType sceneId config title visible infobox { sceneId layerId propertyId property { id ...PropertyFragment __typename } blocks { id pluginId extensionId propertyId property { id ...PropertyFragment __typename } __typename } __typename } isSketch sketch { customPropertySchema featureCollection { type features { ...FeatureFragment __typename } __typename } __typename } ... on NLSLayerGroup { children { id __typename } __typename } __typename}fragment NLSLayerStyle on Style { id name value __typename}", + Variables: map[string]any{ + "sceneId": sId, + "lang": "en", + }, + } + res := Request(e, uID.String(), requestBody) + newLayers := res.Path("$.data.node.newLayers").Array() + newLayers.Length().Equal(1) + return newLayers.Element(0).Object() +} + +func addTestNLSLayerSimple(e *httpexpect.Expect, sId string) string { + requestBody := GraphQLRequest{ + OperationName: "AddNLSLayerSimple", + Query: "mutation AddNLSLayerSimple($input: AddNLSLayerSimpleInput!) { addNLSLayerSimple(input: $input) { layers { id __typename } __typename }}", + Variables: map[string]any{ + "input": map[string]any{ + "sceneId": sId, + "config": map[string]any{ + "properties": map[string]any{ + "name": "TestLayer", + }, + "layerStyleId": "", + "data": map[string]any{ + "type": "geojson", + }, + }, + "visible": true, + "layerType": "simple", + "title": "TestLayer", + "index": 0, + "schema": map[string]any{ + "AAA": "Text_1", + "BBB": "Int_2", + "XXX": "URL_3", + "YYY": "Boolean_4", + }, + }, + }, + } + res := Request(e, uID.String(), requestBody) + return res.Path("$.data.addNLSLayerSimple.layers.id").Raw().(string) +} + +func addTestGeoJSONFeature(e *httpexpect.Expect, lId string, proId string) string { + requestBody := GraphQLRequest{ + OperationName: "AddGeoJSONFeature", + Query: "mutation AddGeoJSONFeature($input: AddGeoJSONFeatureInput!) { addGeoJSONFeature(input: $input) { id type properties __typename }}", + Variables: map[string]any{ + "input": map[string]any{ + "layerId": lId, + "geometry": map[string]any{ + "type": "Point", + "coordinates": []float64{ + 139.75315007107542, + 35.68233694131118, + }, + }, + "type": "Feature", + "properties": map[string]any{ + "id": proId, + "type": "marker", + "positions": [][]float64{ + { + -3958794.0678944187, + 3350992.944211698, + 3699619.2576891473, + }, + }, + "extrudedHeight": 0, + }, + }, + }, + } + res := Request(e, uID.String(), requestBody) + featureId := res.Path("$.data.addGeoJSONFeature.id").Raw().(string) + return featureId +} + +func updateTestGeoJSONFeature(e *httpexpect.Expect, lId string, fId string, proId string) { + requestBody := GraphQLRequest{ + OperationName: "UpdateGeoJSONFeature", + Query: "mutation UpdateGeoJSONFeature($input: UpdateGeoJSONFeatureInput!) { updateGeoJSONFeature(input: $input) { id type properties __typename }}", + Variables: map[string]any{ + "input": map[string]any{ + "layerId": lId, + "featureId": fId, + "geometry": map[string]any{ + "type": "Point", + "coordinates": []float64{ + 139.75315007107542, + 35.68233694131118, + }, + }, + "properties": map[string]any{ + "extrudedHeight": 0, + "id": proId, + "positions": [][]float64{ + { + -3958794.0678944187, + 3350992.944211698, + 3699619.2576891473, + }, + }, + "type": "marker", + "AAA": "aaa", + "BBB": 123, + "XXX": "xxx", + "YYY": true, + }, + }, + }, + } + Request(e, uID.String(), requestBody) +} + +const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + +func RandomString(n int) string { + src := rand.NewSource(time.Now().UnixNano()) + r := rand.New(src) + result := make([]byte, n) + for i := range result { + result[i] = letters[r.Intn(len(letters))] + } + return string(result) +} diff --git a/server/e2e/gql_nlslayer_test.go b/server/e2e/gql_nlslayer_test.go index 67f8b6e0cc..411e1a6a32 100644 --- a/server/e2e/gql_nlslayer_test.go +++ b/server/e2e/gql_nlslayer_test.go @@ -734,15 +734,15 @@ func TestInfoboxBlocksCRUD(t *testing.T) { Value("updatedAt").NotEqual(notUpdatedProjectUpdatedAt) } -func addCustomProperties( +func updateCustomProperties( e *httpexpect.Expect, layerId string, schema map[string]any, ) (GraphQLRequest, *httpexpect.Value) { requestBody := GraphQLRequest{ - OperationName: "AddCustomProperties", - Query: `mutation AddCustomProperties($input: AddCustomPropertySchemaInput!) { - addCustomProperties(input: $input) { + OperationName: "UpdateCustomProperties", + Query: `mutation UpdateCustomProperties($input: UpdateCustomPropertySchemaInput!) { + updateCustomProperties(input: $input) { layer { id sketch { @@ -807,7 +807,7 @@ func TestCustomProperties(t *testing.T) { "extrudedHeight": 0, "positions": []float64{1, 2, 3}, } - addCustomProperties(e, layerId, schema1) + updateCustomProperties(e, layerId, schema1) _, res3 := fetchSceneForNewLayers(e, sId) res3.Object(). @@ -830,7 +830,7 @@ func TestCustomProperties(t *testing.T) { "extrudedHeight": 10, "positions": []float64{4, 5, 6}, } - addCustomProperties(e, layerId, schema2) + updateCustomProperties(e, layerId, schema2) _, res4 := fetchSceneForNewLayers(e, sId) res4.Object(). diff --git a/server/e2e/gql_project_import_test.go b/server/e2e/gql_project_import_test.go index 8b8f3f70b4..b3998e7732 100644 --- a/server/e2e/gql_project_import_test.go +++ b/server/e2e/gql_project_import_test.go @@ -1,13 +1,13 @@ package e2e import ( - "encoding/json" "net/http" "os" "testing" "github.com/gavv/httpexpect/v2" "github.com/reearth/reearth/server/internal/app/config" + "github.com/stretchr/testify/assert" "golang.org/x/text/language" ) @@ -67,12 +67,14 @@ func importProject(t *testing.T, e *httpexpect.Expect, filePath string) *httpexp } }`, } + operations, err := toJSONString(requestBody) + assert.Nil(t, err) r := e.POST("/api/graphql"). WithHeader("Origin", "https://example.com"). WithHeader("authorization", "Bearer test"). WithHeader("X-Reearth-Debug-User", uID.String()). WithMultipart(). - WithFormField("operations", toJSONString(requestBody)). + WithFormField("operations", operations). WithFormField("map", `{"0": ["variables.file"]}`). WithFile("0", filePath). Expect(). @@ -108,11 +110,6 @@ func getScene(e *httpexpect.Expect, s string, l string) *httpexpect.Object { return v.Object() } -func toJSONString(v interface{}) string { - jsonData, _ := json.Marshal(v) - return string(jsonData) -} - const GetSceneGuery = ` query GetScene($sceneId: ID!, $lang: Lang) { node(id: $sceneId, type: SCENE) { diff --git a/server/e2e/gql_storytelling_test.go b/server/e2e/gql_storytelling_test.go index 87cbc66eca..6fb679ecf9 100644 --- a/server/e2e/gql_storytelling_test.go +++ b/server/e2e/gql_storytelling_test.go @@ -1,14 +1,9 @@ package e2e import ( - "bytes" "context" - "encoding/json" "fmt" - "io" "net/http" - "regexp" - "strings" "testing" "github.com/gavv/httpexpect/v2" @@ -40,14 +35,7 @@ func createProject(e *httpexpect.Expect, name string) string { }, } - res := 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() + res := Request(e, uID.String(), requestBody) return res.Path("$.data.createProject.project.id").Raw().(string) } @@ -69,14 +57,7 @@ func createScene(e *httpexpect.Expect, pID string) (GraphQLRequest, *httpexpect. }, } - res := 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() + res := Request(e, uID.String(), requestBody) sID := res.Path("$.data.createScene.scene.id").Raw().(string) return requestBody, res, sID @@ -131,14 +112,7 @@ func fetchSceneForStories(e *httpexpect.Expect, sID string) (GraphQLRequest, *ht }, } - res := e.POST("/api/graphql"). - WithHeader("Origin", "https://example.com"). - WithHeader("X-Reearth-Debug-User", uID.String()). - WithHeader("Content-Type", "application/json"). - WithJSON(fetchSceneRequestBody). - Expect(). - Status(http.StatusOK). - JSON() + res := Request(e, uID.String(), fetchSceneRequestBody) return fetchSceneRequestBody, res } @@ -163,14 +137,7 @@ func createStory(e *httpexpect.Expect, sID, name string, index int) (GraphQLRequ }, } - res := 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() + res := Request(e, uID.String(), requestBody) res.Object(). Value("data").Object(). @@ -204,14 +171,7 @@ func updateStory(e *httpexpect.Expect, storyID, sID string) (GraphQLRequest, *ht }, } - res := 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() + res := Request(e, uID.String(), requestBody) res.Object(). Value("data").Object(). @@ -236,14 +196,7 @@ func deleteStory(e *httpexpect.Expect, storyID, sID string) (GraphQLRequest, *ht }, } - res := 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() + res := Request(e, uID.String(), requestBody) res.Object(). Value("data").Object(). @@ -271,14 +224,7 @@ func publishStory(e *httpexpect.Expect, storyID, alias string) (GraphQLRequest, }, } - res := 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() + res := Request(e, uID.String(), requestBody) res.Object(). Value("data").Object(). @@ -315,14 +261,7 @@ func createPage(e *httpexpect.Expect, sID, storyID, name string, swipeable bool) }, } - res := 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() + res := Request(e, uID.String(), requestBody) res.Object(). Value("data").Object(). @@ -358,14 +297,7 @@ func updatePage(e *httpexpect.Expect, sID, storyID, pageID, name string, swipeab }, } - res := 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() + res := Request(e, uID.String(), requestBody) res.Object(). Value("data").Object(). @@ -399,14 +331,7 @@ func movePage(e *httpexpect.Expect, storyID, pageID string, idx int) (GraphQLReq }, } - res := 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() + res := Request(e, uID.String(), requestBody) res.Object(). Value("data").Object(). @@ -437,14 +362,7 @@ func deletePage(e *httpexpect.Expect, sID, storyID, pageID string) (GraphQLReque }, } - res := 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() + res := Request(e, uID.String(), requestBody) res.Object(). Value("data").Object(). @@ -480,14 +398,7 @@ func duplicatePage(e *httpexpect.Expect, sID, storyID, pageID string) (GraphQLRe }, } - res := 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() + res := Request(e, uID.String(), requestBody) pID := res.Object(). Value("data").Object(). @@ -529,14 +440,7 @@ func addLayerToPage(e *httpexpect.Expect, sId, storyId, pageId, layerId string, }, } - res := 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() + res := Request(e, uID.String(), requestBody) pageRes := res.Object(). Value("data").Object(). @@ -585,14 +489,7 @@ func removeLayerToPage(e *httpexpect.Expect, sId, storyId, pageId, layerId strin }, } - res := 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() + res := Request(e, uID.String(), requestBody) pageRes := res.Object(). Value("data").Object(). @@ -644,14 +541,7 @@ func createBlock(e *httpexpect.Expect, sID, storyID, pageID, pluginId, extension }, } - res := 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() + res := Request(e, uID.String(), requestBody) res.Object(). Value("data").Object(). @@ -691,14 +581,7 @@ func removeBlock(e *httpexpect.Expect, storyID, pageID, blockID string) (GraphQL }, } - res := 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() + res := Request(e, uID.String(), requestBody) res.Object(). Path("$.data.removeStoryBlock.page.blocks[:].id").Array().NotContains(blockID) @@ -736,14 +619,7 @@ func moveBlock(e *httpexpect.Expect, storyID, pageID, blockID string, index int) }, } - res := 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() + res := Request(e, uID.String(), requestBody) res.Object(). Path("$.data.moveStoryBlock.page.blocks[:].id").Array().Contains(blockID) @@ -752,12 +628,7 @@ func moveBlock(e *httpexpect.Expect, storyID, pageID, blockID string, index int) } func TestStoryCRUD(t *testing.T) { - e := StartServer(t, &config.Config{ - Origins: []string{"https://example.com"}, - AuthSrv: config.AuthSrvConfig{ - Disabled: true, - }, - }, true, baseSeeder) + e := Server(t, baseSeeder) pID := createProject(e, "test") @@ -798,12 +669,7 @@ func TestStoryCRUD(t *testing.T) { } func TestStoryPageCRUD(t *testing.T) { - e := StartServer(t, &config.Config{ - Origins: []string{"https://example.com"}, - AuthSrv: config.AuthSrvConfig{ - Disabled: true, - }, - }, true, baseSeeder) + e := Server(t, baseSeeder) pID := createProject(e, "test") @@ -839,14 +705,7 @@ func TestStoryPageCRUD(t *testing.T) { // update page with invalid page id requestBody.Variables["pageId"] = id.NewPageID().String() - res = 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() + res = Request(e, uID.String(), requestBody) res.Object(). Value("errors").Array(). @@ -892,12 +751,7 @@ func TestStoryPageCRUD(t *testing.T) { } func TestStoryPageLayersCRUD(t *testing.T) { - e := StartServer(t, &config.Config{ - Origins: []string{"https://example.com"}, - AuthSrv: config.AuthSrvConfig{ - Disabled: true, - }, - }, true, baseSeeder) + e := Server(t, baseSeeder) pID := createProject(e, "test") @@ -929,12 +783,7 @@ func TestStoryPageLayersCRUD(t *testing.T) { } func TestStoryPageBlocksCRUD(t *testing.T) { - e := StartServer(t, &config.Config{ - Origins: []string{"https://example.com"}, - AuthSrv: config.AuthSrvConfig{ - Disabled: true, - }, - }, true, baseSeeder) + e := Server(t, baseSeeder) pID := createProject(e, "test") @@ -977,12 +826,7 @@ func TestStoryPageBlocksCRUD(t *testing.T) { } func TestStoryPageBlocksProperties(t *testing.T) { - e := StartServer(t, &config.Config{ - Origins: []string{"https://example.com"}, - AuthSrv: config.AuthSrvConfig{ - Disabled: true, - }, - }, true, baseSeeder) + e := Server(t, baseSeeder) pID := createProject(e, "test") @@ -1138,7 +982,7 @@ func TestStoryPublishing(t *testing.T) { ] }`, sID, storyID, extensionId, blockID, pageID, pluginId) - RegexpJSONEq(t, rc, expected) + RegexpJSONEReadCloser(t, rc, expected) resString := e.GET("/p/test-alias/data.json"). WithHeader("Origin", "https://example.com"). @@ -1147,52 +991,6 @@ func TestStoryPublishing(t *testing.T) { Body(). Raw() - RegexpJSONEqStr(t, resString, expected) - -} - -func RegexpJSONEq(t *testing.T, rc io.ReadCloser, expected string) { - - buf := new(bytes.Buffer) - _, err := buf.ReadFrom(rc) - assert.NoError(t, err) - - var obj1 map[string]interface{} - err = json.Unmarshal(buf.Bytes(), &obj1) - assert.Nil(t, err) - - obj2, err := json.Marshal(obj1) - assert.Nil(t, err) - - var obj3 map[string]interface{} - err = json.Unmarshal([]byte(expected), &obj3) - assert.Nil(t, err) - - obj4, err := json.Marshal(obj3) - assert.Nil(t, err) - - pub := regexp.MustCompile(strings.ReplaceAll(string(obj4), "[", "\\[")) - - assert.Regexp(t, pub, string(obj2)) -} - -func RegexpJSONEqStr(t *testing.T, buf string, expected string) { - - var obj1 map[string]interface{} - err := json.Unmarshal([]byte(buf), &obj1) - assert.Nil(t, err) - - obj2, err := json.Marshal(obj1) - assert.Nil(t, err) - - var obj3 map[string]interface{} - err = json.Unmarshal([]byte(expected), &obj3) - assert.Nil(t, err) - - obj4, err := json.Marshal(obj3) - assert.Nil(t, err) - - pub := regexp.MustCompile(strings.ReplaceAll(string(obj4), "[", "\\[")) + JSONEqRegexp(t, resString, expected) - assert.Regexp(t, pub, string(obj2)) } diff --git a/server/go.mod b/server/go.mod index 93bef2bd96..a4f11c43a4 100644 --- a/server/go.mod +++ b/server/go.mod @@ -172,5 +172,3 @@ require ( ) go 1.23.4 - -toolchain go1.23.4 diff --git a/server/gql/newlayer.graphql b/server/gql/newlayer.graphql index 48ea640c33..10b441e7a8 100644 --- a/server/gql/newlayer.graphql +++ b/server/gql/newlayer.graphql @@ -128,14 +128,22 @@ input DuplicateNLSLayerInput { layerId: ID! } -input AddCustomPropertySchemaInput { +input UpdateCustomPropertySchemaInput { layerId: ID! schema: JSON } -input UpdateCustomPropertySchemaInput { +input ChangeCustomPropertyTitleInput { layerId: ID! schema: JSON + oldTitle: String! + newTitle: String! +} + +input RemoveCustomPropertyInput { + layerId: ID! + schema: JSON + removedTitle: String! } # Payload @@ -199,10 +207,13 @@ extend type Mutation { input: RemoveNLSInfoboxBlockInput! ): RemoveNLSInfoboxBlockPayload duplicateNLSLayer(input: DuplicateNLSLayerInput!): DuplicateNLSLayerPayload! - addCustomProperties( - input: AddCustomPropertySchemaInput! - ): UpdateNLSLayerPayload! updateCustomProperties( input: UpdateCustomPropertySchemaInput! ): UpdateNLSLayerPayload! + changeCustomPropertyTitle( + input: ChangeCustomPropertyTitleInput! + ): UpdateNLSLayerPayload! + removeCustomProperty( + input: RemoveCustomPropertyInput! + ): UpdateNLSLayerPayload! } diff --git a/server/internal/adapter/gql/generated.go b/server/internal/adapter/gql/generated.go index ff7fc5476a..e9a598eab3 100644 --- a/server/internal/adapter/gql/generated.go +++ b/server/internal/adapter/gql/generated.go @@ -625,7 +625,6 @@ type ComplexityRoot struct { Mutation struct { AddCluster func(childComplexity int, input gqlmodel.AddClusterInput) int - AddCustomProperties func(childComplexity int, input gqlmodel.AddCustomPropertySchemaInput) int AddDatasetSchema func(childComplexity int, input gqlmodel.AddDatasetSchemaInput) int AddGeoJSONFeature func(childComplexity int, input gqlmodel.AddGeoJSONFeatureInput) int AddInfoboxField func(childComplexity int, input gqlmodel.AddInfoboxFieldInput) int @@ -640,6 +639,7 @@ type ComplexityRoot struct { AddWidget func(childComplexity int, input gqlmodel.AddWidgetInput) int AttachTagItemToGroup func(childComplexity int, input gqlmodel.AttachTagItemToGroupInput) int AttachTagToLayer func(childComplexity int, input gqlmodel.AttachTagToLayerInput) int + ChangeCustomPropertyTitle func(childComplexity int, input gqlmodel.ChangeCustomPropertyTitleInput) int CreateAsset func(childComplexity int, input gqlmodel.CreateAssetInput) int CreateInfobox func(childComplexity int, input gqlmodel.CreateInfoboxInput) int CreateNLSInfobox func(childComplexity int, input gqlmodel.CreateNLSInfoboxInput) int @@ -679,6 +679,7 @@ type ComplexityRoot struct { PublishStory func(childComplexity int, input gqlmodel.PublishStoryInput) int RemoveAsset func(childComplexity int, input gqlmodel.RemoveAssetInput) int RemoveCluster func(childComplexity int, input gqlmodel.RemoveClusterInput) int + RemoveCustomProperty func(childComplexity int, input gqlmodel.RemoveCustomPropertyInput) int RemoveDatasetSchema func(childComplexity int, input gqlmodel.RemoveDatasetSchemaInput) int RemoveInfobox func(childComplexity int, input gqlmodel.RemoveInfoboxInput) int RemoveInfoboxField func(childComplexity int, input gqlmodel.RemoveInfoboxFieldInput) int @@ -1575,8 +1576,9 @@ type MutationResolver interface { MoveNLSInfoboxBlock(ctx context.Context, input gqlmodel.MoveNLSInfoboxBlockInput) (*gqlmodel.MoveNLSInfoboxBlockPayload, error) RemoveNLSInfoboxBlock(ctx context.Context, input gqlmodel.RemoveNLSInfoboxBlockInput) (*gqlmodel.RemoveNLSInfoboxBlockPayload, error) DuplicateNLSLayer(ctx context.Context, input gqlmodel.DuplicateNLSLayerInput) (*gqlmodel.DuplicateNLSLayerPayload, error) - AddCustomProperties(ctx context.Context, input gqlmodel.AddCustomPropertySchemaInput) (*gqlmodel.UpdateNLSLayerPayload, error) UpdateCustomProperties(ctx context.Context, input gqlmodel.UpdateCustomPropertySchemaInput) (*gqlmodel.UpdateNLSLayerPayload, error) + ChangeCustomPropertyTitle(ctx context.Context, input gqlmodel.ChangeCustomPropertyTitleInput) (*gqlmodel.UpdateNLSLayerPayload, error) + RemoveCustomProperty(ctx context.Context, input gqlmodel.RemoveCustomPropertyInput) (*gqlmodel.UpdateNLSLayerPayload, error) InstallPlugin(ctx context.Context, input gqlmodel.InstallPluginInput) (*gqlmodel.InstallPluginPayload, error) UninstallPlugin(ctx context.Context, input gqlmodel.UninstallPluginInput) (*gqlmodel.UninstallPluginPayload, error) UploadPlugin(ctx context.Context, input gqlmodel.UploadPluginInput) (*gqlmodel.UploadPluginPayload, error) @@ -3939,18 +3941,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.AddCluster(childComplexity, args["input"].(gqlmodel.AddClusterInput)), true - case "Mutation.addCustomProperties": - if e.complexity.Mutation.AddCustomProperties == nil { - break - } - - args, err := ec.field_Mutation_addCustomProperties_args(context.TODO(), rawArgs) - if err != nil { - return 0, false - } - - return e.complexity.Mutation.AddCustomProperties(childComplexity, args["input"].(gqlmodel.AddCustomPropertySchemaInput)), true - case "Mutation.addDatasetSchema": if e.complexity.Mutation.AddDatasetSchema == nil { break @@ -4119,6 +4109,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.AttachTagToLayer(childComplexity, args["input"].(gqlmodel.AttachTagToLayerInput)), true + case "Mutation.changeCustomPropertyTitle": + if e.complexity.Mutation.ChangeCustomPropertyTitle == nil { + break + } + + args, err := ec.field_Mutation_changeCustomPropertyTitle_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.ChangeCustomPropertyTitle(childComplexity, args["input"].(gqlmodel.ChangeCustomPropertyTitleInput)), true + case "Mutation.createAsset": if e.complexity.Mutation.CreateAsset == nil { break @@ -4587,6 +4589,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.RemoveCluster(childComplexity, args["input"].(gqlmodel.RemoveClusterInput)), true + case "Mutation.removeCustomProperty": + if e.complexity.Mutation.RemoveCustomProperty == nil { + break + } + + args, err := ec.field_Mutation_removeCustomProperty_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.RemoveCustomProperty(childComplexity, args["input"].(gqlmodel.RemoveCustomPropertyInput)), true + case "Mutation.removeDatasetSchema": if e.complexity.Mutation.RemoveDatasetSchema == nil { break @@ -8335,7 +8349,6 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec := executionContext{rc, e, 0, 0, make(chan graphql.DeferredResult)} inputUnmarshalMap := graphql.BuildUnmarshalerMap( ec.unmarshalInputAddClusterInput, - ec.unmarshalInputAddCustomPropertySchemaInput, ec.unmarshalInputAddDatasetSchemaInput, ec.unmarshalInputAddGeoJSONFeatureInput, ec.unmarshalInputAddInfoboxFieldInput, @@ -8350,6 +8363,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputAssetSort, ec.unmarshalInputAttachTagItemToGroupInput, ec.unmarshalInputAttachTagToLayerInput, + ec.unmarshalInputChangeCustomPropertyTitleInput, ec.unmarshalInputCreateAssetInput, ec.unmarshalInputCreateInfoboxInput, ec.unmarshalInputCreateNLSInfoboxInput, @@ -8393,6 +8407,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputPublishStoryInput, ec.unmarshalInputRemoveAssetInput, ec.unmarshalInputRemoveClusterInput, + ec.unmarshalInputRemoveCustomPropertyInput, ec.unmarshalInputRemoveDatasetSchemaInput, ec.unmarshalInputRemoveInfoboxFieldInput, ec.unmarshalInputRemoveInfoboxInput, @@ -9432,14 +9447,22 @@ input DuplicateNLSLayerInput { layerId: ID! } -input AddCustomPropertySchemaInput { +input UpdateCustomPropertySchemaInput { + layerId: ID! + schema: JSON +} + +input ChangeCustomPropertyTitleInput { layerId: ID! schema: JSON + oldTitle: String! + newTitle: String! } -input UpdateCustomPropertySchemaInput { +input RemoveCustomPropertyInput { layerId: ID! schema: JSON + removedTitle: String! } # Payload @@ -9503,12 +9526,15 @@ extend type Mutation { input: RemoveNLSInfoboxBlockInput! ): RemoveNLSInfoboxBlockPayload duplicateNLSLayer(input: DuplicateNLSLayerInput!): DuplicateNLSLayerPayload! - addCustomProperties( - input: AddCustomPropertySchemaInput! - ): UpdateNLSLayerPayload! updateCustomProperties( input: UpdateCustomPropertySchemaInput! ): UpdateNLSLayerPayload! + changeCustomPropertyTitle( + input: ChangeCustomPropertyTitleInput! + ): UpdateNLSLayerPayload! + removeCustomProperty( + input: RemoveCustomPropertyInput! + ): UpdateNLSLayerPayload! } `, BuiltIn: false}, {Name: "../../../gql/plugin.graphql", Input: `type Plugin { @@ -10988,21 +11014,6 @@ func (ec *executionContext) field_Mutation_addCluster_args(ctx context.Context, return args, nil } -func (ec *executionContext) field_Mutation_addCustomProperties_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { - var err error - args := map[string]interface{}{} - var arg0 gqlmodel.AddCustomPropertySchemaInput - if tmp, ok := rawArgs["input"]; ok { - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) - arg0, err = ec.unmarshalNAddCustomPropertySchemaInput2githubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐAddCustomPropertySchemaInput(ctx, tmp) - if err != nil { - return nil, err - } - } - args["input"] = arg0 - return args, nil -} - func (ec *executionContext) field_Mutation_addDatasetSchema_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -11213,6 +11224,21 @@ func (ec *executionContext) field_Mutation_attachTagToLayer_args(ctx context.Con return args, nil } +func (ec *executionContext) field_Mutation_changeCustomPropertyTitle_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 gqlmodel.ChangeCustomPropertyTitleInput + if tmp, ok := rawArgs["input"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) + arg0, err = ec.unmarshalNChangeCustomPropertyTitleInput2githubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐChangeCustomPropertyTitleInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_createAsset_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -11798,6 +11824,21 @@ func (ec *executionContext) field_Mutation_removeCluster_args(ctx context.Contex return args, nil } +func (ec *executionContext) field_Mutation_removeCustomProperty_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 gqlmodel.RemoveCustomPropertyInput + if tmp, ok := rawArgs["input"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) + arg0, err = ec.unmarshalNRemoveCustomPropertyInput2githubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐRemoveCustomPropertyInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_Mutation_removeDatasetSchema_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -31497,8 +31538,8 @@ func (ec *executionContext) fieldContext_Mutation_duplicateNLSLayer(ctx context. return fc, nil } -func (ec *executionContext) _Mutation_addCustomProperties(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Mutation_addCustomProperties(ctx, field) +func (ec *executionContext) _Mutation_updateCustomProperties(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_updateCustomProperties(ctx, field) if err != nil { return graphql.Null } @@ -31511,7 +31552,7 @@ func (ec *executionContext) _Mutation_addCustomProperties(ctx context.Context, f }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().AddCustomProperties(rctx, fc.Args["input"].(gqlmodel.AddCustomPropertySchemaInput)) + return ec.resolvers.Mutation().UpdateCustomProperties(rctx, fc.Args["input"].(gqlmodel.UpdateCustomPropertySchemaInput)) }) if err != nil { ec.Error(ctx, err) @@ -31528,7 +31569,7 @@ func (ec *executionContext) _Mutation_addCustomProperties(ctx context.Context, f return ec.marshalNUpdateNLSLayerPayload2ᚖgithubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐUpdateNLSLayerPayload(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Mutation_addCustomProperties(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Mutation_updateCustomProperties(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Mutation", Field: field, @@ -31549,15 +31590,15 @@ func (ec *executionContext) fieldContext_Mutation_addCustomProperties(ctx contex } }() ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Mutation_addCustomProperties_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + if fc.Args, err = ec.field_Mutation_updateCustomProperties_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { ec.Error(ctx, err) return fc, err } return fc, nil } -func (ec *executionContext) _Mutation_updateCustomProperties(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Mutation_updateCustomProperties(ctx, field) +func (ec *executionContext) _Mutation_changeCustomPropertyTitle(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_changeCustomPropertyTitle(ctx, field) if err != nil { return graphql.Null } @@ -31570,7 +31611,7 @@ func (ec *executionContext) _Mutation_updateCustomProperties(ctx context.Context }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().UpdateCustomProperties(rctx, fc.Args["input"].(gqlmodel.UpdateCustomPropertySchemaInput)) + return ec.resolvers.Mutation().ChangeCustomPropertyTitle(rctx, fc.Args["input"].(gqlmodel.ChangeCustomPropertyTitleInput)) }) if err != nil { ec.Error(ctx, err) @@ -31587,7 +31628,7 @@ func (ec *executionContext) _Mutation_updateCustomProperties(ctx context.Context return ec.marshalNUpdateNLSLayerPayload2ᚖgithubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐUpdateNLSLayerPayload(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Mutation_updateCustomProperties(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Mutation_changeCustomPropertyTitle(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Mutation", Field: field, @@ -31608,7 +31649,66 @@ func (ec *executionContext) fieldContext_Mutation_updateCustomProperties(ctx con } }() ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Mutation_updateCustomProperties_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + if fc.Args, err = ec.field_Mutation_changeCustomPropertyTitle_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_removeCustomProperty(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_removeCustomProperty(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.Mutation().RemoveCustomProperty(rctx, fc.Args["input"].(gqlmodel.RemoveCustomPropertyInput)) + }) + 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.UpdateNLSLayerPayload) + fc.Result = res + return ec.marshalNUpdateNLSLayerPayload2ᚖgithubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐUpdateNLSLayerPayload(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_removeCustomProperty(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "layer": + return ec.fieldContext_UpdateNLSLayerPayload_layer(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type UpdateNLSLayerPayload", 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_Mutation_removeCustomProperty_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { ec.Error(ctx, err) return fc, err } @@ -59393,40 +59493,6 @@ func (ec *executionContext) unmarshalInputAddClusterInput(ctx context.Context, o return it, nil } -func (ec *executionContext) unmarshalInputAddCustomPropertySchemaInput(ctx context.Context, obj interface{}) (gqlmodel.AddCustomPropertySchemaInput, error) { - var it gqlmodel.AddCustomPropertySchemaInput - asMap := map[string]interface{}{} - for k, v := range obj.(map[string]interface{}) { - asMap[k] = v - } - - fieldsInOrder := [...]string{"layerId", "schema"} - for _, k := range fieldsInOrder { - v, ok := asMap[k] - if !ok { - continue - } - switch k { - case "layerId": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("layerId")) - data, err := ec.unmarshalNID2githubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐID(ctx, v) - if err != nil { - return it, err - } - it.LayerID = data - case "schema": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("schema")) - data, err := ec.unmarshalOJSON2githubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐJSON(ctx, v) - if err != nil { - return it, err - } - it.Schema = data - } - } - - return it, nil -} - func (ec *executionContext) unmarshalInputAddDatasetSchemaInput(ctx context.Context, obj interface{}) (gqlmodel.AddDatasetSchemaInput, error) { var it gqlmodel.AddDatasetSchemaInput asMap := map[string]interface{}{} @@ -60099,6 +60165,54 @@ func (ec *executionContext) unmarshalInputAttachTagToLayerInput(ctx context.Cont return it, nil } +func (ec *executionContext) unmarshalInputChangeCustomPropertyTitleInput(ctx context.Context, obj interface{}) (gqlmodel.ChangeCustomPropertyTitleInput, error) { + var it gqlmodel.ChangeCustomPropertyTitleInput + asMap := map[string]interface{}{} + for k, v := range obj.(map[string]interface{}) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"layerId", "schema", "oldTitle", "newTitle"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "layerId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("layerId")) + data, err := ec.unmarshalNID2githubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐID(ctx, v) + if err != nil { + return it, err + } + it.LayerID = data + case "schema": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("schema")) + data, err := ec.unmarshalOJSON2githubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐJSON(ctx, v) + if err != nil { + return it, err + } + it.Schema = data + case "oldTitle": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("oldTitle")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.OldTitle = data + case "newTitle": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("newTitle")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.NewTitle = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputCreateAssetInput(ctx context.Context, obj interface{}) (gqlmodel.CreateAssetInput, error) { var it gqlmodel.CreateAssetInput asMap := map[string]interface{}{} @@ -61827,6 +61941,47 @@ func (ec *executionContext) unmarshalInputRemoveClusterInput(ctx context.Context return it, nil } +func (ec *executionContext) unmarshalInputRemoveCustomPropertyInput(ctx context.Context, obj interface{}) (gqlmodel.RemoveCustomPropertyInput, error) { + var it gqlmodel.RemoveCustomPropertyInput + asMap := map[string]interface{}{} + for k, v := range obj.(map[string]interface{}) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"layerId", "schema", "removedTitle"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "layerId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("layerId")) + data, err := ec.unmarshalNID2githubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐID(ctx, v) + if err != nil { + return it, err + } + it.LayerID = data + case "schema": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("schema")) + data, err := ec.unmarshalOJSON2githubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐJSON(ctx, v) + if err != nil { + return it, err + } + it.Schema = data + case "removedTitle": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("removedTitle")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.RemovedTitle = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputRemoveDatasetSchemaInput(ctx context.Context, obj interface{}) (gqlmodel.RemoveDatasetSchemaInput, error) { var it gqlmodel.RemoveDatasetSchemaInput asMap := map[string]interface{}{} @@ -70288,16 +70443,23 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { out.Invalids++ } - case "addCustomProperties": + case "updateCustomProperties": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_updateCustomProperties(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "changeCustomPropertyTitle": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { - return ec._Mutation_addCustomProperties(ctx, field) + return ec._Mutation_changeCustomPropertyTitle(ctx, field) }) if out.Values[i] == graphql.Null { out.Invalids++ } - case "updateCustomProperties": + case "removeCustomProperty": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { - return ec._Mutation_updateCustomProperties(ctx, field) + return ec._Mutation_removeCustomProperty(ctx, field) }) if out.Values[i] == graphql.Null { out.Invalids++ @@ -78172,11 +78334,6 @@ func (ec *executionContext) unmarshalNAddClusterInput2githubᚗcomᚋreearthᚋr return res, graphql.ErrorOnPath(ctx, err) } -func (ec *executionContext) unmarshalNAddCustomPropertySchemaInput2githubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐAddCustomPropertySchemaInput(ctx context.Context, v interface{}) (gqlmodel.AddCustomPropertySchemaInput, error) { - res, err := ec.unmarshalInputAddCustomPropertySchemaInput(ctx, v) - return res, graphql.ErrorOnPath(ctx, err) -} - func (ec *executionContext) unmarshalNAddDatasetSchemaInput2githubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐAddDatasetSchemaInput(ctx context.Context, v interface{}) (gqlmodel.AddDatasetSchemaInput, error) { res, err := ec.unmarshalInputAddDatasetSchemaInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) @@ -78397,6 +78554,11 @@ func (ec *executionContext) marshalNBoolean2bool(ctx context.Context, sel ast.Se return res } +func (ec *executionContext) unmarshalNChangeCustomPropertyTitleInput2githubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐChangeCustomPropertyTitleInput(ctx context.Context, v interface{}) (gqlmodel.ChangeCustomPropertyTitleInput, error) { + res, err := ec.unmarshalInputChangeCustomPropertyTitleInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) marshalNCluster2ᚕᚖgithubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐClusterᚄ(ctx context.Context, sel ast.SelectionSet, v []*gqlmodel.Cluster) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup @@ -80931,6 +81093,11 @@ func (ec *executionContext) unmarshalNRemoveClusterInput2githubᚗcomᚋreearth return res, graphql.ErrorOnPath(ctx, err) } +func (ec *executionContext) unmarshalNRemoveCustomPropertyInput2githubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐRemoveCustomPropertyInput(ctx context.Context, v interface{}) (gqlmodel.RemoveCustomPropertyInput, error) { + res, err := ec.unmarshalInputRemoveCustomPropertyInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) unmarshalNRemoveDatasetSchemaInput2githubᚗcomᚋreearthᚋreearthᚋserverᚋinternalᚋadapterᚋgqlᚋgqlmodelᚐRemoveDatasetSchemaInput(ctx context.Context, v interface{}) (gqlmodel.RemoveDatasetSchemaInput, error) { res, err := ec.unmarshalInputRemoveDatasetSchemaInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) diff --git a/server/internal/adapter/gql/gqlmodel/models_gen.go b/server/internal/adapter/gql/gqlmodel/models_gen.go index b579dcbde4..5eee5f86a7 100644 --- a/server/internal/adapter/gql/gqlmodel/models_gen.go +++ b/server/internal/adapter/gql/gqlmodel/models_gen.go @@ -84,11 +84,6 @@ type AddClusterPayload struct { Cluster *Cluster `json:"cluster"` } -type AddCustomPropertySchemaInput struct { - LayerID ID `json:"layerId"` - Schema JSON `json:"schema,omitempty"` -} - type AddDatasetSchemaInput struct { SceneID ID `json:"sceneId"` Name string `json:"name"` @@ -275,6 +270,13 @@ type Camera struct { Fov float64 `json:"fov"` } +type ChangeCustomPropertyTitleInput struct { + LayerID ID `json:"layerId"` + Schema JSON `json:"schema,omitempty"` + OldTitle string `json:"oldTitle"` + NewTitle string `json:"newTitle"` +} + type Cluster struct { ID ID `json:"id"` Name string `json:"name"` @@ -1388,6 +1390,12 @@ type RemoveClusterPayload struct { ClusterID ID `json:"clusterId"` } +type RemoveCustomPropertyInput struct { + LayerID ID `json:"layerId"` + Schema JSON `json:"schema,omitempty"` + RemovedTitle string `json:"removedTitle"` +} + type RemoveDatasetSchemaInput struct { SchemaID ID `json:"schemaId"` Force *bool `json:"force,omitempty"` diff --git a/server/internal/adapter/gql/resolver_mutation_nlslayer.go b/server/internal/adapter/gql/resolver_mutation_nlslayer.go index ee2d946438..c2f8cc96a0 100644 --- a/server/internal/adapter/gql/resolver_mutation_nlslayer.go +++ b/server/internal/adapter/gql/resolver_mutation_nlslayer.go @@ -218,7 +218,7 @@ func (r *mutationResolver) RemoveNLSInfoboxBlock(ctx context.Context, input gqlm }, nil } -func (r *mutationResolver) AddCustomProperties(ctx context.Context, input gqlmodel.AddCustomPropertySchemaInput) (*gqlmodel.UpdateNLSLayerPayload, error) { +func (r *mutationResolver) UpdateCustomProperties(ctx context.Context, input gqlmodel.UpdateCustomPropertySchemaInput) (*gqlmodel.UpdateNLSLayerPayload, error) { lid, err := gqlmodel.ToID[id.NLSLayer](input.LayerID) if err != nil { return nil, err @@ -237,16 +237,35 @@ func (r *mutationResolver) AddCustomProperties(ctx context.Context, input gqlmod }, nil } -func (r *mutationResolver) UpdateCustomProperties(ctx context.Context, input gqlmodel.UpdateCustomPropertySchemaInput) (*gqlmodel.UpdateNLSLayerPayload, error) { +func (r *mutationResolver) ChangeCustomPropertyTitle(ctx context.Context, input gqlmodel.ChangeCustomPropertyTitleInput) (*gqlmodel.UpdateNLSLayerPayload, error) { lid, err := gqlmodel.ToID[id.NLSLayer](input.LayerID) if err != nil { return nil, err } - layer, err := usecases(ctx).NLSLayer.AddOrUpdateCustomProperties(ctx, interfaces.AddOrUpdateCustomPropertiesInput{ + layer, err := usecases(ctx).NLSLayer.ChangeCustomPropertyTitle(ctx, interfaces.AddOrUpdateCustomPropertiesInput{ LayerID: lid, Schema: input.Schema, - }, getOperator(ctx)) + }, input.OldTitle, input.NewTitle, getOperator(ctx)) + if err != nil { + return nil, err + } + + return &gqlmodel.UpdateNLSLayerPayload{ + Layer: gqlmodel.ToNLSLayer(layer, nil), + }, nil +} + +func (r *mutationResolver) RemoveCustomProperty(ctx context.Context, input gqlmodel.RemoveCustomPropertyInput) (*gqlmodel.UpdateNLSLayerPayload, error) { + lid, err := gqlmodel.ToID[id.NLSLayer](input.LayerID) + if err != nil { + return nil, err + } + + layer, err := usecases(ctx).NLSLayer.RemoveCustomProperty(ctx, interfaces.AddOrUpdateCustomPropertiesInput{ + LayerID: lid, + Schema: input.Schema, + }, input.RemovedTitle, getOperator(ctx)) if err != nil { return nil, err } diff --git a/server/internal/app/config/config.go b/server/internal/app/config/config.go index 7215d00fd2..62ddd9ea0e 100644 --- a/server/internal/app/config/config.go +++ b/server/internal/app/config/config.go @@ -29,9 +29,9 @@ type Config struct { Host_Web string `pp:",omitempty"` Dev bool `pp:",omitempty"` DB string `default:"mongodb://localhost"` - DB_Account string `pp:",omitempty"` + DB_Account string `default:"reearth_account" pp:",omitempty"` DB_Users []appx.NamedURI `pp:",omitempty"` - DB_Vis string `pp:",omitempty"` + DB_Vis string `default:"reearth" pp:",omitempty"` GraphQL GraphQLConfig `pp:",omitempty"` Published PublishedConfig `pp:",omitempty"` GCPProject string `envconfig:"GOOGLE_CLOUD_PROJECT" pp:",omitempty"` diff --git a/server/internal/app/repo.go b/server/internal/app/repo.go index 7fa55f2b06..80d9b8567d 100644 --- a/server/internal/app/repo.go +++ b/server/internal/app/repo.go @@ -25,31 +25,9 @@ import ( "go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo" ) -const defaultVisDatabase = "reearth" -const defaultAccountDatabase = "reearth_account" - -func initReposAndGateways(ctx context.Context, conf *config.Config, debug bool) (*repo.Container, *gateway.Container, *accountrepo.Container, *accountgateway.Container) { - gateways := &gateway.Container{} - acGateways := &accountgateway.Container{} - - // Mongo - client, err := mongo.Connect( - ctx, - options.Client(). - ApplyURI(conf.DB). - SetMonitor(otelmongo.NewMonitor()), - ) - if err != nil { - log.Fatalf("mongo error: %+v\n", err) - } - - // for account database +func initAccountDatabase(client *mongo.Client, txAvailable bool, ctx context.Context, conf *config.Config) *accountrepo.Container { accountDatabase := conf.DB_Account - accountRepoCompat := false - if accountDatabase == "" { - accountDatabase = defaultAccountDatabase - accountRepoCompat = true - } + log.Infof("accountDatabase: %s", accountDatabase) accountUsers := make([]accountrepo.User, 0, len(conf.DB_Users)) for _, u := range conf.DB_Users { @@ -60,23 +38,44 @@ func initReposAndGateways(ctx context.Context, conf *config.Config, debug bool) accountUsers = append(accountUsers, accountmongo.NewUserWithHost(mongox.NewClient(accountDatabase, c), u.Name)) } - txAvailable := mongox.IsTransactionAvailable(conf.DB) - - accountRepos, err := accountmongo.New(ctx, client, accountDatabase, txAvailable, accountRepoCompat, accountUsers) + // this flag is for old database structure compatibility + // on this service, it is always false + useLegacyStructure := false + accountRepos, err := accountmongo.New(ctx, client, accountDatabase, txAvailable, useLegacyStructure, accountUsers) if err != nil { - log.Fatalf("Failed to init mongo: %+v\n", err) + log.Fatalf("Failed to init mongo database account: %+v\n", err) } + return accountRepos +} - // for reearth visualizer database +func initVisDatabase(client *mongo.Client, txAvailable bool, accountRepos *accountrepo.Container, ctx context.Context, conf *config.Config) *repo.Container { visDatabase := conf.DB_Vis - if visDatabase == "" { - visDatabase = defaultVisDatabase - } + log.Infof("visDatabase: %s", visDatabase) repos, err := mongorepo.NewWithExtensions(ctx, client.Database(visDatabase), accountRepos, txAvailable, conf.Ext_Plugin) if err != nil { - log.Fatalf("Failed to init mongo: %+v\n", err) + log.Fatalf("Failed to init mongo database visualizer: %+v\n", err) } + return repos +} + +func initReposAndGateways(ctx context.Context, conf *config.Config, debug bool) (*repo.Container, *gateway.Container, *accountrepo.Container, *accountgateway.Container) { + gateways := &gateway.Container{} + acGateways := &accountgateway.Container{} + + // Mongo + client, err := mongo.Connect( + ctx, + options.Client(). + ApplyURI(conf.DB). + SetMonitor(otelmongo.NewMonitor()), + ) + if err != nil { + log.Fatalf("mongo error: %+v\n", err) + } + txAvailable := mongox.IsTransactionAvailable(conf.DB) + accountRepos := initAccountDatabase(client, txAvailable, ctx, conf) + visRepos := initVisDatabase(client, txAvailable, accountRepos, ctx, conf) // File gateways.File = initFile(ctx, conf) @@ -99,11 +98,11 @@ func initReposAndGateways(ctx context.Context, conf *config.Config, debug bool) } // release lock of all scenes - if err := repos.SceneLock.ReleaseAllLock(context.Background()); err != nil { + if err := visRepos.SceneLock.ReleaseAllLock(context.Background()); err != nil { log.Fatalf("repo initialization error: %v", err) } - return repos, gateways, accountRepos, acGateways + return visRepos, gateways, accountRepos, acGateways } func initFile(ctx context.Context, conf *config.Config) (fileRepo gateway.File) { diff --git a/server/internal/usecase/interactor/nlslayer.go b/server/internal/usecase/interactor/nlslayer.go index d55017edaf..361a59a8dc 100644 --- a/server/internal/usecase/interactor/nlslayer.go +++ b/server/internal/usecase/interactor/nlslayer.go @@ -3,6 +3,7 @@ package interactor import ( "context" "errors" + "fmt" "net/url" "strings" @@ -258,6 +259,12 @@ func (i *NLSLayer) Update(ctx context.Context, inp interfaces.UpdateNLSLayerInpu if inp.Name != nil { layer.Rename(*inp.Name) + if config := layer.Config(); config != nil { + c := *config + if properties, ok := c["properties"].(map[string]interface{}); ok { + properties["name"] = *inp.Name + } + } } if inp.Visible != nil { @@ -646,6 +653,9 @@ func (i *NLSLayer) AddOrUpdateCustomProperties(ctx context.Context, inp interfac if err != nil { return nil, err } + if err := i.CanWriteScene(layer.Scene(), operator); err != nil { + return nil, interfaces.ErrOperationDenied + } if layer.Sketch() == nil { featureCollection := nlslayer.NewFeatureCollection( @@ -677,6 +687,138 @@ func (i *NLSLayer) AddOrUpdateCustomProperties(ctx context.Context, inp interfac return layer, nil } +func (i *NLSLayer) ChangeCustomPropertyTitle(ctx context.Context, inp interfaces.AddOrUpdateCustomPropertiesInput, oldTitle string, newTitle string, operator *usecase.Operator) (_ nlslayer.NLSLayer, err error) { + tx, err := i.transaction.Begin(ctx) + if err != nil { + return + } + + ctx = tx.Context() + defer func() { + if err2 := tx.End(ctx); err == nil && err2 != nil { + err = err2 + } + }() + + layer, err := i.nlslayerRepo.FindByID(ctx, inp.LayerID) + if err != nil { + return nil, err + } + + if layer.Sketch() == nil || layer.Sketch().FeatureCollection() == nil { + return nil, interfaces.ErrSketchNotFound + } + if err := i.CanWriteScene(layer.Scene(), operator); err != nil { + return nil, interfaces.ErrOperationDenied + } + + // Check if oldTitle exists and newTitle doesn't conflict + titleExists := false + for _, feature := range layer.Sketch().FeatureCollection().Features() { + if props := feature.Properties(); props != nil { + if _, ok := (*props)[oldTitle]; ok { + titleExists = true + } + if _, ok := (*props)[newTitle]; ok { + return nil, fmt.Errorf("property with title %s already exists", newTitle) + } + } + } + + if !titleExists { + return nil, fmt.Errorf("property with title %s not found", oldTitle) + } + + for _, feature := range layer.Sketch().FeatureCollection().Features() { + if props := feature.Properties(); props != nil { + for k, v := range *props { + if k == oldTitle { + value := v + delete(*props, k) + (*props)[newTitle] = value + } + } + } + } + + layer.Sketch().SetCustomPropertySchema(&inp.Schema) + + err = i.nlslayerRepo.Save(ctx, layer) + if err != nil { + return nil, err + } + + err = updateProjectUpdatedAtByScene(ctx, layer.Scene(), i.projectRepo, i.sceneRepo) + if err != nil { + return nil, err + } + + tx.Commit() + return layer, nil +} + +func (i *NLSLayer) RemoveCustomProperty(ctx context.Context, inp interfaces.AddOrUpdateCustomPropertiesInput, removedTitle string, operator *usecase.Operator) (_ nlslayer.NLSLayer, err error) { + tx, err := i.transaction.Begin(ctx) + if err != nil { + return + } + + ctx = tx.Context() + defer func() { + if err2 := tx.End(ctx); err == nil && err2 != nil { + err = err2 + } + }() + + layer, err := i.nlslayerRepo.FindByID(ctx, inp.LayerID) + if err != nil { + return nil, err + } + if layer.Sketch() == nil || layer.Sketch().FeatureCollection() == nil { + return nil, interfaces.ErrSketchNotFound + } + + // Check if removedTitle exists + titleExists := false + for _, feature := range layer.Sketch().FeatureCollection().Features() { + if props := feature.Properties(); props != nil { + if _, ok := (*props)[removedTitle]; ok { + titleExists = true + break + } + } + } + + if !titleExists { + return nil, fmt.Errorf("property with title %s not found", removedTitle) + } + + for _, feature := range layer.Sketch().FeatureCollection().Features() { + if props := feature.Properties(); props != nil { + for k := range *props { + if k == removedTitle { + delete(*props, k) + } + } + } + } + + layer.Sketch().SetCustomPropertySchema(&inp.Schema) + + err = i.nlslayerRepo.Save(ctx, layer) + if err != nil { + return nil, err + } + + err = updateProjectUpdatedAtByScene(ctx, layer.Scene(), i.projectRepo, i.sceneRepo) + if err != nil { + return nil, err + } + + tx.Commit() + return layer, nil +} + func (i *NLSLayer) AddGeoJSONFeature(ctx context.Context, inp interfaces.AddNLSLayerGeoJSONFeatureParams, operator *usecase.Operator) (_ nlslayer.Feature, err error) { tx, err := i.transaction.Begin(ctx) if err != nil { diff --git a/server/internal/usecase/interfaces/nlslayer.go b/server/internal/usecase/interfaces/nlslayer.go index aaa5a672f3..9acf392f42 100644 --- a/server/internal/usecase/interfaces/nlslayer.go +++ b/server/internal/usecase/interfaces/nlslayer.go @@ -85,6 +85,8 @@ type NLSLayer interface { RemoveNLSInfoboxBlock(context.Context, RemoveNLSInfoboxBlockParam, *usecase.Operator) (id.InfoboxBlockID, nlslayer.NLSLayer, error) Duplicate(context.Context, id.NLSLayerID, *usecase.Operator) (nlslayer.NLSLayer, error) AddOrUpdateCustomProperties(context.Context, AddOrUpdateCustomPropertiesInput, *usecase.Operator) (nlslayer.NLSLayer, error) + ChangeCustomPropertyTitle(context.Context, AddOrUpdateCustomPropertiesInput, string, string, *usecase.Operator) (nlslayer.NLSLayer, error) + RemoveCustomProperty(context.Context, AddOrUpdateCustomPropertiesInput, string, *usecase.Operator) (nlslayer.NLSLayer, error) AddGeoJSONFeature(context.Context, AddNLSLayerGeoJSONFeatureParams, *usecase.Operator) (nlslayer.Feature, error) UpdateGeoJSONFeature(context.Context, UpdateNLSLayerGeoJSONFeatureParams, *usecase.Operator) (nlslayer.Feature, error) DeleteGeoJSONFeature(context.Context, DeleteNLSLayerGeoJSONFeatureParams, *usecase.Operator) (id.FeatureID, error) diff --git a/web/package.json b/web/package.json index 7f614fa18f..152289bf8b 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "@reearth/visualizer", - "version": "1.0.0-beta.2.0", + "version": "1.0.0-beta.2.1", "repository": "https://github.com/reearth/reearth-visualizer.git", "author": "Re:Earth contributors ", "license": "Apache-2.0", diff --git a/web/src/beta/features/PluginPlayground/Code/hook.ts b/web/src/beta/features/PluginPlayground/Code/hook.ts index 6958c6c77e..a476a90562 100644 --- a/web/src/beta/features/PluginPlayground/Code/hook.ts +++ b/web/src/beta/features/PluginPlayground/Code/hook.ts @@ -3,7 +3,9 @@ import { useNotification } from "@reearth/services/state"; import * as yaml from "js-yaml"; import { ComponentProps, useCallback, useState } from "react"; +import { Story } from "../../Visualizer/Crust/StoryPanel/types"; import { WidgetLocation } from "../../Visualizer/Crust/Widgets/types"; +import { DEFAULT_LAYERS_PLUGIN_PLAYGROUND } from "../LayerList/constants"; import { FileType } from "../Plugins/constants"; type Widgets = ComponentProps["widgets"]; @@ -28,6 +30,17 @@ type ReearthYML = { }[]; }; +type CustomInfoboxBlock = { + id: string; + name: string; + description: string; + __REEARTH_SOURCECODE: string; + extensionId: string; + pluginId: string; +}; + +type CustomStoryBlock = CustomInfoboxBlock; + type Props = { files: FileType[]; }; @@ -50,72 +63,14 @@ const getYmlJson = (file: FileType) => { }; export default ({ files }: Props) => { + const [infoboxBlocks, setInfoboxBlocks] = useState(); + const [story, setStory] = useState(); const [widgets, setWidgets] = useState(); const [, setNotification] = useNotification(); - const [fileOutputs, setFileOutputs] = useState< - { - title: string; - output: string; - }[] - >(); const executeCode = useCallback(() => { const ymlFile = files.find((file) => file.title.endsWith(".yml")); - const jsFiles = files.filter((file) => file.title.endsWith(".js")); - - const outputs = jsFiles.map((file) => { - try { - const fn = new Function( - `"use strict"; - const reearth = { - ui: { - show: function () {} - }, - popup: { - show: function () {} - }, - modal: { - show: function () {} - } - }; - - let capturedConsole = []; - - console.log = (message) => { - capturedConsole.push(message); - }; - - console.error = (message) => { - capturedConsole.push(message); - }; - - ${file.sourceCode}; - - return capturedConsole.join("\\n"); - ` - ); - - return { - title: file.title, - output: fn() - }; - } catch (error) { - if (error instanceof Error) { - return { - title: file.title, - output: error.message - }; - } - return { - title: file.title, - output: "Failed to execute" - }; - } - }); - - setFileOutputs(outputs); - if (!ymlFile) return; const getYmlResult = getYmlJson(ymlFile); @@ -182,11 +137,74 @@ export default ({ files }: Props) => { } ); setWidgets(widgets); + + const infoboBlockFromExtension = ymlJson.extensions.reduce< + CustomInfoboxBlock[] + >((prv, cur) => { + if (cur.type !== "infoboxBlock") return prv; + + const file = files.find((file) => file.title === `${cur.id}.js`); + + if (!file) { + return prv; + } + + return [ + ...prv, + { + id: cur.id, + name: cur.name, + description: cur.description, + __REEARTH_SOURCECODE: file.sourceCode, + extensionId: cur.id, + pluginId: cur.id + } + ]; + }, []); + + setInfoboxBlocks(infoboBlockFromExtension); + + const storyBlocksFromExtension = ymlJson.extensions.reduce< + CustomStoryBlock[] + >((prv, cur) => { + if (cur.type !== "storyBlock") return prv; + + const file = files.find((file) => file.title === `${cur.id}.js`); + + if (!file) { + return prv; + } + + prv.push({ + id: cur.id, + name: cur.name, + description: cur.description, + __REEARTH_SOURCECODE: file.sourceCode, + extensionId: cur.id, + pluginId: cur.id + }); + return prv; + }, []); + + setStory({ + id: "story", + title: "First Story", + position: "left", + bgColor: "#f0f0f0", + pages: [ + { + id: "page", + blocks: storyBlocksFromExtension, + layerIds: DEFAULT_LAYERS_PLUGIN_PLAYGROUND.map((l) => l.id) + } + ] + }); }, [files, setNotification]); return { executeCode, - widgets, - fileOutputs + infoboxBlocks, + story, + widgets }; }; diff --git a/web/src/beta/features/PluginPlayground/LayerList/constants.ts b/web/src/beta/features/PluginPlayground/LayerList/constants.ts index 91a6f0ee2f..4408926611 100644 --- a/web/src/beta/features/PluginPlayground/LayerList/constants.ts +++ b/web/src/beta/features/PluginPlayground/LayerList/constants.ts @@ -3,18 +3,17 @@ import { Layer } from "@reearth/core"; export const DEFAULT_LAYERS_PLUGIN_PLAYGROUND: Layer[] = [ { type: "simple", - id: "01jff2ww37r9msq4an8mvxh409", + id: "1", title: "chiyoda 3D tiles", visible: true, data: { type: "3dtiles", url: "https://assets.cms.plateau.reearth.io/assets/11/6d05db-ed47-4f88-b565-9eb385b1ebb0/13100_tokyo23-ku_2022_3dtiles%20_1_1_op_bldg_13101_chiyoda-ku_lod1/tileset.json" - }, - "3dtiles": {} + } }, { type: "simple", - id: "01jff3fxh13h2trxstqfx363nx", + id: "2", title: "japan-heritage", visible: true, data: { diff --git a/web/src/beta/features/PluginPlayground/LayerList/index.tsx b/web/src/beta/features/PluginPlayground/LayerList/index.tsx index 3239b33be4..76cf511a3a 100644 --- a/web/src/beta/features/PluginPlayground/LayerList/index.tsx +++ b/web/src/beta/features/PluginPlayground/LayerList/index.tsx @@ -1,18 +1,22 @@ import { Layer, MapRef } from "@reearth/core"; import { styled } from "@reearth/services/theme"; -import { FC, MutableRefObject, useCallback, useState } from "react"; +import { FC, MutableRefObject, useCallback } from "react"; import LayerItem from "./LayerItem"; type Props = { handleLayerVisibilityUpdate: (layerId: string, visible: boolean) => void; layers: Layer[]; + selectedLayerId: string; + setSelectedLayerId: (layerId: string) => void; visualizerRef: MutableRefObject; }; const LayerList: FC = ({ handleLayerVisibilityUpdate, layers, + selectedLayerId, + setSelectedLayerId, visualizerRef }) => { const handleFlyTo = useCallback( @@ -22,8 +26,6 @@ const LayerList: FC = ({ [visualizerRef] ); - const [selectedLayerId, setSelectedLayerId] = useState(""); - return ( {layers && diff --git a/web/src/beta/features/PluginPlayground/Plugins/index.tsx b/web/src/beta/features/PluginPlayground/Plugins/index.tsx index 18b9317519..61f959db94 100644 --- a/web/src/beta/features/PluginPlayground/Plugins/index.tsx +++ b/web/src/beta/features/PluginPlayground/Plugins/index.tsx @@ -1,4 +1,4 @@ -import { Collapse, IconButton } from "@reearth/beta/lib/reearth-ui"; +import { Collapse, IconButton, Typography } from "@reearth/beta/lib/reearth-ui"; import { EntryItem } from "@reearth/beta/ui/components"; import { styled } from "@reearth/services/styled"; import { FC, useState } from "react"; @@ -40,16 +40,11 @@ const Plugins: FC = ({ }) => { const [isAddingNewFile, setIsAddingNewFile] = useState(false); - const handleShareIconClicked = (): void => { + const handlePluginShare = (): void => { if (!selectedPlugin) return; encodeAndSharePlugin(selectedPlugin.id); }; - const customPlugin = presetPlugins.find((plugin) => plugin.id === "custom"); - const pluginsWithoutCustom = presetPlugins.filter( - (plugin) => plugin.id !== "custom" - ); - const PluginEntryItem: FC<{ plugin: { id: string; title: string }; selectedPluginId: string; @@ -85,53 +80,62 @@ const Plugins: FC = ({ - {customPlugin && ( + {sharedPlugin && (
- {customPlugin.plugins.map((plugin) => ( - - ))} -
- )} - {pluginsWithoutCustom.map((category) => ( - - {category.plugins.map((plugin) => ( + - ))} + + + + )} + {presetPlugins.map((category) => ( +
+ + + {category.plugins.length > 0 ? ( + category.plugins.map((plugin) => ( + + )) + ) : ( + + + No plugins + + + )} + - +
))} -
- Shared - {sharedPlugin && ( - - )} -
@@ -175,17 +179,26 @@ const PluginList = styled("div")(({ theme }) => ({ paddingRight: theme.spacing.small, display: "flex", flexDirection: "column", - gap: theme.spacing.small + gap: theme.spacing.smallest +})); + +const PluginListWrapper = styled("div")(({ theme }) => ({ + display: "flex", + height: "100%", + marginLeft: -theme.spacing.smallest })); -const PluginListWrapper = styled("div")(() => ({ +const PluginSubList = styled("div")(({ theme }) => ({ display: "flex", - height: "100%" + flexDirection: "column", + gap: theme.spacing.smallest, + paddingLeft: 24, + paddingTop: theme.spacing.smallest })); -const CategoryTitle = styled("div")(({ theme }) => ({ - padding: `${theme.spacing.smallest}px 0`, - fontSize: theme.fonts.sizes.body +const EmptyTip = styled("div")(({ theme }) => ({ + padding: theme.spacing.smallest, + paddingLeft: theme.spacing.small })); const FileListWrapper = styled("div")(({ theme }) => ({ @@ -210,8 +223,4 @@ const IconList = styled("div")(({ theme }) => ({ marginBottom: theme.spacing.small })); -const PresetPluginWrapper = styled("div")(({ theme }) => ({ - marginLeft: -theme.spacing.normal -})); - export default Plugins; diff --git a/web/src/beta/features/PluginPlayground/Plugins/presets/custom/myPlugin.ts b/web/src/beta/features/PluginPlayground/Plugins/presets/custom/myPlugin.ts index 714c37eb48..f4b8a68909 100644 --- a/web/src/beta/features/PluginPlayground/Plugins/presets/custom/myPlugin.ts +++ b/web/src/beta/features/PluginPlayground/Plugins/presets/custom/myPlugin.ts @@ -17,6 +17,15 @@ extensions: zone: outer section: left area: top + - id: demo-infobox-block-1 + type: infoboxBlock + name: Demo Infobox Block 1 + - id: demo-infobox-block-2 + type: infoboxBlock + name: Demo Infobox Block 2 + - id: demo-story-block + type: storyBlock + name: Demo Story Block `, disableEdit: true, disableDelete: true @@ -33,8 +42,47 @@ const widgetFile: FileType = { \`); ` }; +const demoInfoboxBlock1File: FileType = { + id: "custom-my-plugin-demo-infobox-block-1", + title: "demo-infobox-block-1.js", + sourceCode: `reearth.ui.show(\` + ${PRESET_PLUGIN_COMMON_STYLE} +
+

Infobox Block 1

+
+\`); ` +}; + +const demoInfoboxBlock2File: FileType = { + id: "custom-my-plugin-demo-infobox-block-2", + title: "demo-infobox-block-2.js", + sourceCode: `reearth.ui.show(\` + ${PRESET_PLUGIN_COMMON_STYLE} +
+

Infobox Block 2

+
+\`); ` +}; + +const demoStoryFile: FileType = { + id: "custom-my-plugin-demo-story", + title: "demo-story-block.js", + sourceCode: `reearth.ui.show(\` + ${PRESET_PLUGIN_COMMON_STYLE} +
+

Demo Story

+
+\`); ` +}; + export const myPlugin: PluginType = { id: "my-plugin", title: "My Plugin", - files: [widgetFile, yamlFile] + files: [ + widgetFile, + demoInfoboxBlock1File, + demoInfoboxBlock2File, + demoStoryFile, + yamlFile + ] }; diff --git a/web/src/beta/features/PluginPlayground/Plugins/presets/index.ts b/web/src/beta/features/PluginPlayground/Plugins/presets/index.ts index 78ba633070..2fbde97c86 100644 --- a/web/src/beta/features/PluginPlayground/Plugins/presets/index.ts +++ b/web/src/beta/features/PluginPlayground/Plugins/presets/index.ts @@ -1,9 +1,17 @@ import { PluginType } from "../constants"; import { myPlugin } from "./custom/myPlugin"; +import { addGeojson } from "./layers/add-geojson"; +import { addCzml } from "./layers/add-czml"; +import { addKml } from "./layers/add-kml"; +import { addCsv } from "./layers/add-csv"; +import { add3dTiles } from "./layers/add-3Dtiles"; +import { addOsm3dTiles } from "./layers/add-OSM-3DTiles"; +import { addWms } from "./layers/add-wms"; import { header } from "./ui/header"; import { responsivePanel } from "./ui/responsivePanel"; import { sidebar } from "./ui/sidebar"; +import { uiExtensionMessenger } from "./ui/uiExtensionMessenger"; type PresetPluginCategory = { id: string; @@ -21,7 +29,56 @@ export const presetPlugins: PresetPlugins = [ }, { id: "ui", - title: "UI", + title: "User Interface", plugins: [responsivePanel, sidebar, header] + }, + { + id: "communication", + title: "Communication", + plugins: [uiExtensionMessenger] + }, + { + id: "viewerScene", + title: "Viewer & Scene Settings", + plugins: [] + }, + { + id: "layers", + title: "Manage Layer", + plugins: [ + addGeojson, + addCzml, + addKml, + addCsv, + add3dTiles, + addOsm3dTiles, + addWms + ] + }, + { + id: "layerStyles", + title: "Manage Layer Style", + plugins: [] + }, + + { + id: "camera", + title: "Camera", + plugins: [] + }, + { + id: "timeline", + title: "Timeline", + plugins: [] + }, + { + id: "dataStorage", + title: "Data Storage", + plugins: [] + }, + { + id: "sketch", + title: "Sketch", + plugins: [] } ]; diff --git a/web/src/beta/features/PluginPlayground/Plugins/presets/layers/add-3Dtiles.ts b/web/src/beta/features/PluginPlayground/Plugins/presets/layers/add-3Dtiles.ts new file mode 100644 index 0000000000..d235f4cbde --- /dev/null +++ b/web/src/beta/features/PluginPlayground/Plugins/presets/layers/add-3Dtiles.ts @@ -0,0 +1,69 @@ +import { FileType, PluginType } from "../../constants"; + +const yamlFile: FileType = { + id: "layers-add-3dTiles-reearth-yml", + title: "reearth.yml", + sourceCode: `id: layers-add-3dTiles-plugin +name: Add 3D Tiles +version: 1.0.0 +extensions: + - id: layers-add-3dTiles + type: widget + name: Add 3D Tiles + description: Add 3D Tiles + `, + disableEdit: true, + disableDelete: true +}; + +const widgetFile: FileType = { + id: "layers-add-3dTiles", + title: "layers-add-3dTiles.js", + sourceCode: `// Example of adding a layer with 3D Tiles data + +// Define 3D Tiles +const layer3dTiles = { + type: "simple", // Required + data: { + type: "3dtiles", + url: "https://assets.cms.plateau.reearth.io/assets/8b/cce097-2d4a-46eb-a98b-a78e7178dc30/13103_minato-ku_pref_2023_citygml_1_op_bldg_3dtiles_13103_minato-ku_lod2_no_texture/tileset.json", // URL of 3D Tiles + }, + "3dtiles": { // Settings for the 3D Tiles style. + pbr: false, //invalid Physically Based Rendering + selectedFeatureColor: "red", // If you select a feature, it will change color + }, +}; + +// Add the 3D Tiles layer from the URL to Re:Earth +reearth.layers.add(layer3dTiles); + +// Enable Terrain +reearth.viewer.overrideProperty({ + terrain: { + enabled: true, + }, +}); + +// Move the camera to the position where the CZML data is displayed. +reearth.camera.flyTo( + // Define the camera position to be moved to + { + heading: 4.022965234428543, + height: 1616.524859060678, + lat: 35.67170282368589, + lng: 139.7707144962995, + pitch: -0.464517599879275, + roll: 6.283168638897022, + }, + // Define camera movement time + { + duration: 2.0, + } +);` +}; + +export const add3dTiles: PluginType = { + id: "add-3dTiles", + title: "Add 3D Tiles", + files: [widgetFile, yamlFile] +}; diff --git a/web/src/beta/features/PluginPlayground/Plugins/presets/layers/add-OSM-3DTiles.ts b/web/src/beta/features/PluginPlayground/Plugins/presets/layers/add-OSM-3DTiles.ts new file mode 100644 index 0000000000..c48507623a --- /dev/null +++ b/web/src/beta/features/PluginPlayground/Plugins/presets/layers/add-OSM-3DTiles.ts @@ -0,0 +1,65 @@ +import { FileType, PluginType } from "../../constants"; + +const yamlFile: FileType = { + id: "layers-add-osm3dTiles-reearth-yml", + title: "reearth.yml", + sourceCode: `id: layers-add-osm3dTiles-plugin +name: Add OSM 3D Tiles +version: 1.0.0 +extensions: + - id: layers-add-osm3dTiles + type: widget + name: Add OSM 3D Tiles + description: Add OSM 3D Tiles + `, + disableEdit: true, + disableDelete: true +}; + +const widgetFile: FileType = { + id: "layers-add-osm3dTiles", + title: "layers-add-osm3dTiles.js", + sourceCode: `// Example of adding a layer with OSM 3D Tiles data + +// Define OSM 3D Tiles +const layerOsm3dTiles = { + type: "simple", // Required + data: { + type: "osm-buildings", // Data type + }, +}; + +// Add the OSM 3D Tiles layer from the URL to Re:Earth +reearth.layers.add(layerOsm3dTiles); + +// Enable Terrain +reearth.viewer.overrideProperty({ + terrain: { + enabled: true, + }, +}); + +// Move the camera to the position where the OSM 3D Tiles data is displayed. +reearth.camera.flyTo( + // Define the camera position to be moved to + { + heading: 0.20219047310022553, + height: 261.79910347824375, + lat: 44.13880442335244, + lng: 4.8038131598778, + pitch: -0.5139201681525183, + roll: 0.000011404713798235377, + }, + // Define camera movement time + { + duration: 2.0, + } +); +` +}; + +export const addOsm3dTiles: PluginType = { + id: "add-osm3dTiles", + title: "Add OSM 3D Tiles", + files: [widgetFile, yamlFile] +}; diff --git a/web/src/beta/features/PluginPlayground/Plugins/presets/layers/add-csv.ts b/web/src/beta/features/PluginPlayground/Plugins/presets/layers/add-csv.ts new file mode 100644 index 0000000000..f32e8aa1b8 --- /dev/null +++ b/web/src/beta/features/PluginPlayground/Plugins/presets/layers/add-csv.ts @@ -0,0 +1,47 @@ +import { FileType, PluginType } from "../../constants"; + +const yamlFile: FileType = { + id: "layers-add-csv-reearth-yml", + title: "reearth.yml", + sourceCode: `id: layers-add-csv-plugin +name: Add CSV +version: 1.0.0 +extensions: + - id: layers-add-csv + type: widget + name: Add CSV + description: Add CSV + `, + disableEdit: true, + disableDelete: true +}; + +const widgetFile: FileType = { + id: "layers-add-csv", + title: "layers-add-csv.js", + sourceCode: `// Example of adding a layer with CSV data + +// Define CSV data +const layerCsv = { + type: "simple", // Required + data: { + type: "csv", + url: "https://reearth.github.io/visualizer-plugin-sample-data/public/csv/marker.csv", // URL of CSV + csv: { // Define by column name + lngColumn: "longitude", + latColumn: "latitude", + }, + }, + marker: {},// Settings for the feature style. This statement is required even if no style is set. +}; + +// Add the CSV layer from the URL to Re:Earth +reearth.layers.add(layerCsv); +` +}; + +export const addCsv: PluginType = { + id: "add-csv", + title: "Add CSV", + files: [widgetFile, yamlFile] +}; diff --git a/web/src/beta/features/PluginPlayground/Plugins/presets/layers/add-czml.ts b/web/src/beta/features/PluginPlayground/Plugins/presets/layers/add-czml.ts new file mode 100644 index 0000000000..20689b85cd --- /dev/null +++ b/web/src/beta/features/PluginPlayground/Plugins/presets/layers/add-czml.ts @@ -0,0 +1,101 @@ +import { FileType, PluginType } from "../../constants"; + +const yamlFile: FileType = { + id: "layers-add-czml-reearth-yml", + title: "reearth.yml", + sourceCode: `id: layers-add-czml-plugin +name: Add CZML +version: 1.0.0 +extensions: + - id: layers-add-czml + type: widget + name: Add CZML + description: Add CZML + `, + disableEdit: true, + disableDelete: true +}; + +const widgetFile: FileType = { + id: "layers-add-czml", + title: "layers-add-czml.js", + sourceCode: `// Example of adding a layer with CZML data + +// Define the CZML array +const czmlData = [ + { + id: "document", + name: "3D Polygon Example", + version: "1.0", + }, + { + id: "polygon", + name: "Polygon Example", + polygon: { + positions: { + cartographicDegrees: [ + -95.19644404998762, 39.121855688403606, 0, -95.86066795468007, + 30.135315689161864, 0, -84.60676001423798, 29.013591974308632, 0, + -82.7088322952643, 37.87660535441917, 0, -95.19644404998762, + 39.121855688403606, 0, + ], + }, + extrudedHeight: 300000, + }, + }, +]; + +// Convert the CZML array to a JSON string, then encode it, and make a data URI +const czmlString = JSON.stringify(czmlData); +const encodedCzml = "data:text/plain;charset=UTF-8," + encodeURIComponent(czmlString); + +// Define a layer using the encoded CZML +const layerCzmlEncoded = { + type: "simple", // Required + data: { + type: "czml", // Data format + url: encodedCzml, // Use the encoded CZML string as a data URI + }, + // Write the features style + polygon: { + fillColor: "red", + }, +}; + +// Define a layer using the CZML from an external URL +const layerCzmlUrl = { + type: "simple", + data: { + type: "czml", + url: "https://reearth.github.io/visualizer-plugin-sample-data/public/czml/square_3d_polygon.czml", // URL of CZML file + }, +}; + +// Add the encoded CZML layer to Re:Earth +reearth.layers.add(layerCzmlEncoded); + +// Add the CZML layer from the URL to Re:Earth +reearth.layers.add(layerCzmlUrl); + +// Move the camera to the position where the CZML data is displayed +reearth.camera.flyTo( + { + lng: -88.93602871895675, + lat: 15.453904059215354, + height: 2479417.0253900583, + heading: 5.949278757227463, + pitch: -0.8795938234403176, + roll: 0.0004975556834603267, + }, + { + duration: 2.0, // Define camera movement time + } +); +` +}; + +export const addCzml: PluginType = { + id: "add-czml", + title: "Add CZML", + files: [widgetFile, yamlFile] +}; diff --git a/web/src/beta/features/PluginPlayground/Plugins/presets/layers/add-geojson.ts b/web/src/beta/features/PluginPlayground/Plugins/presets/layers/add-geojson.ts new file mode 100644 index 0000000000..4cbbd4ef2e --- /dev/null +++ b/web/src/beta/features/PluginPlayground/Plugins/presets/layers/add-geojson.ts @@ -0,0 +1,108 @@ +import { FileType, PluginType } from "../../constants"; + +const yamlFile: FileType = { + id: "layers-add-geojson-reearth-yml", + title: "reearth.yml", + sourceCode: `id: layers-add-geojson-plugin +name: Add GeoJSON +version: 1.0.0 +extensions: + - id: layers-add-geojson + type: widget + name: Add Geojson + description: Add Geojson + `, + disableEdit: true, + disableDelete: true +}; + +const widgetFile: FileType = { + id: "layers-add-geojson", + title: "layers-add-geojson.js", + sourceCode: `// Example of adding a layer with GeoJSON data + +// Define the GeoJSON inline +const layerGeojsonInline = { + type: "simple", // Required + data: { + type: "geojson", // Write the data format + value: { + // Ensure that "value" contains GeoJSON + type: "FeatureCollection", + features: [ + { + type: "Feature", + properties: {}, + geometry: { + type: "Polygon", + coordinates: [ + [ + [-97.52842673316115, 28.604966534790364], + [-97.52842673316115, 10.990084521105842], + [-82.13620852840572, 10.990084521105842], + [-82.13620852840572, 28.604966534790364], + [-97.52842673316115, 28.604966534790364], + ], + ], + }, + }, + { + type: "Feature", + properties: {}, + geometry: { + coordinates: [ + [-96.37001899616324, 41.04652707558762], + [-79.17331346249145, 40.45826161216959], + ], + type: "LineString", + }, + }, + { + type: "Feature", + properties: {}, + geometry: { + coordinates: [-111.99963039093615, 19.881084591317787], + type: "Point", + }, + }, + ], + }, + }, + // Settings for the feature style. This statement is required even if no style is set. + polygon: {}, + polyline: {}, + marker: {}, +}; + +// Difine the GeoJSON with URL +const layerGeojsonFromUrl = { + type: "simple", + data: { + type: "geojson", + // URL of GeoJSON file + url: "https://reearth.github.io/visualizer-plugin-sample-data/public/geojson/sample_polygon_polyline_marker.geojson", + }, + polygon: { + fillColor: 'red' + }, + polyline: { + strokeColor: 'red' + }, + marker: { + imageColor: 'red' + }, +}; + +// Add the inline GeoJSON layer to Re:Earth +reearth.layers.add(layerGeojsonInline); + +// Add the GeoJSON layer from the URL to Re:Earth +reearth.layers.add(layerGeojsonFromUrl); +` +}; + +export const addGeojson: PluginType = { + id: "add-geojson", + title: "Add GeoJSON", + files: [widgetFile, yamlFile] +}; diff --git a/web/src/beta/features/PluginPlayground/Plugins/presets/layers/add-kml.ts b/web/src/beta/features/PluginPlayground/Plugins/presets/layers/add-kml.ts new file mode 100644 index 0000000000..cad31cf14c --- /dev/null +++ b/web/src/beta/features/PluginPlayground/Plugins/presets/layers/add-kml.ts @@ -0,0 +1,82 @@ +import { FileType, PluginType } from "../../constants"; + +const yamlFile: FileType = { + id: "layers-add-kml-reearth-yml", + title: "reearth.yml", + sourceCode: `id: layers-add-kml-plugin +name: Add KML +version: 1.0.0 +extensions: + - id: layers-add-kml + type: widget + name: Add KML + description: Add KML + `, + disableEdit: true, + disableDelete: true +}; + +const widgetFile: FileType = { + id: "layers-add-kml", + title: "layers-add-kml.js", + sourceCode: `// Example of adding a layer with KML data + +// Define the KML as a normal string +const kmlData = \` + + + Polygon + + + + + -84.69104794134181,55.71976583858575,0 + -50.21926684954955,54.914116182535565,0 + -59.0296334789341,10.029277658732568,0 + -79.10835968601603,10.498477077010023,0 + -84.69104794134181,55.71976583858575,0 + + + + + +\`; + +// Encode the KML string and set the data URI scheme +const encodedKml = "data:text/plain;charset=UTF-8," + encodeURIComponent(kmlData); + +// Define layers using encoded KML +const layerKmlEncoded = { + type: "simple", // Required + data: { + type: "kml", // Data format + url: encodedKml, // Use the encoded KML string as a data URI + }, + polygon: { // Write the features style + fillColor: "red", + }, +}; + +// Define the KML with URL +const layerKmlUrl = { + type: "simple", + data: { + type: "kml", + url: "https://reearth.github.io/visualizer-plugin-sample-data/public/kml/square.kml", // URL of KML file + }, + polygon: {} +}; + +// Add the encoded KML layer to Re:Earth +reearth.layers.add(layerKmlEncoded); + +// Add the KML layer from the URL to Re:Earth +reearth.layers.add(layerKmlUrl);`, + +}; + +export const addKml: PluginType = { + id: "add-kml", + title: "Add KML", + files: [widgetFile, yamlFile] +}; diff --git a/web/src/beta/features/PluginPlayground/Plugins/presets/layers/add-wms.ts b/web/src/beta/features/PluginPlayground/Plugins/presets/layers/add-wms.ts new file mode 100644 index 0000000000..68d5d785eb --- /dev/null +++ b/web/src/beta/features/PluginPlayground/Plugins/presets/layers/add-wms.ts @@ -0,0 +1,44 @@ +import { FileType, PluginType } from "../../constants"; + +const yamlFile: FileType = { + id: "layers-add-wms-reearth-yml", + title: "reearth.yml", + sourceCode: `id: layers-add-wms-plugin +name: Add wms +version: 1.0.0 +extensions: + - id: layers-add-wms + type: widget + name: Add wms + description: Add wms + `, + disableEdit: true, + disableDelete: true +}; + +const widgetFile: FileType = { + id: "layers-add-wms", + title: "layers-add-wms.js", + sourceCode: `// Define the WMS(Web Map Service) +const layerWmsUrl = { + type: "simple", // Required + data: { + type: "wms", // Data type + // URL of MWS(This data shows Human_Footprint 1995-2004 provided by NASA) + url: "https://gibs.earthdata.nasa.gov/wms/epsg4326/best/wms.cgi", + // Define layer name of WMS + layers: ["Human_Footprint_1995-2004"], + }, +}; + +// Add the WMS layer from the URL to Re:Earth +reearth.layers.add(layerWmsUrl); + +//WMS data is provided by NASA GIBS(https://www.earthdata.nasa.gov/engage/open-data-services-software/earthdata-developer-portal/gibs-api)` +}; + +export const addWms: PluginType = { + id: "add-wms", + title: "Add WMS (Web Map Service)", + files: [widgetFile, yamlFile] +}; diff --git a/web/src/beta/features/PluginPlayground/Plugins/presets/ui/uiExtensionMessenger.ts b/web/src/beta/features/PluginPlayground/Plugins/presets/ui/uiExtensionMessenger.ts new file mode 100644 index 0000000000..f732dfec20 --- /dev/null +++ b/web/src/beta/features/PluginPlayground/Plugins/presets/ui/uiExtensionMessenger.ts @@ -0,0 +1,123 @@ +import { FileType, PluginType } from "../../constants"; +import { PRESET_PLUGIN_COMMON_STYLE } from "../common"; + +const yamlFile: FileType = { + id: "ui-extension-messenger-reearth-yml", + title: "reearth.yml", + sourceCode: `id: ui-extension-messenger-plugin +name: UI Extension Messenger +version: 1.0.0 +extensions: + - id: ui-extension-messenger + type: widget + name: UI Extension Messenger Widget + description: UI Extension Messenger Widget + widgetLayout: + defaultLocation: + zone: outer + section: center + area: top + `, + disableEdit: true, + disableDelete: true +}; + +const widgetFile: FileType = { + id: "ui-extension-messenger-widget", + title: "ui-extension-messenger.js", + sourceCode: `reearth.ui.show(\` + ${PRESET_PLUGIN_COMMON_STYLE} + + +
+

Coordinate Viewer

+
+

Latitude: -°

+

Longitude: -°

+
+
+ +
+
+ + +\`); + +// Send message to UI when globe is clicked +reearth.viewer.on("click", (event) => { + reearth.ui.postMessage({ + type: "position", + lat: event.lat, + lng: event.lng + }); +}); + +// Handle messages from UI to move camera +reearth.extension.on("message", msg => { + if (msg.type === "fly") { + reearth.camera.flyTo( + { + lat: msg.lat, + lng: msg.lng, + height: msg.alt + }, + { duration: 2 } + ); + } +});` +}; + +export const uiExtensionMessenger: PluginType = { + id: "ui-extension-messenger", + title: "UI Extension Messenger", + files: [widgetFile, yamlFile] +}; diff --git a/web/src/beta/features/PluginPlayground/Plugins/usePlugins.ts b/web/src/beta/features/PluginPlayground/Plugins/usePlugins.ts index ab1dd35912..faba2a8ae8 100644 --- a/web/src/beta/features/PluginPlayground/Plugins/usePlugins.ts +++ b/web/src/beta/features/PluginPlayground/Plugins/usePlugins.ts @@ -27,7 +27,11 @@ export default () => { const sharedPlugin = sharedPluginUrl ? (() => { try { - return decodePluginURL(sharedPluginUrl); + const decoded = decodePluginURL(sharedPluginUrl); + return { + ...decoded, + id: `shared-${decoded.id}` + }; } catch (_error) { setNotification({ type: "error", text: "Invalid shared plugin URL" }); return null; @@ -63,6 +67,7 @@ export default () => { const selectPlugin = useCallback((pluginId: string) => { setSelectedPluginId(pluginId); + setSelectedFileId(""); }, []); const selectFile = useCallback((fileId: string) => { diff --git a/web/src/beta/features/PluginPlayground/SettingsList/index.tsx b/web/src/beta/features/PluginPlayground/SettingsList/index.tsx new file mode 100644 index 0000000000..5d2df3287f --- /dev/null +++ b/web/src/beta/features/PluginPlayground/SettingsList/index.tsx @@ -0,0 +1,59 @@ +import { CheckBox, Typography } from "@reearth/beta/lib/reearth-ui"; +import { styled } from "@reearth/services/theme"; +import { FC } from "react"; + +type Props = { + infoboxEnabled: boolean; + setInfoboxEnabled: (infoBoxEnabled: boolean) => void; + setShowStoryPanel: (showStoryPanel: boolean) => void; + showStoryPanel: boolean; +}; +const SettingsList: FC = ({ + infoboxEnabled, + setInfoboxEnabled, + setShowStoryPanel, + showStoryPanel +}) => { + return ( + + + setInfoboxEnabled(!infoboxEnabled)} + /> + + Enable Infobox + + + + setShowStoryPanel(!showStoryPanel)} + /> + + Enable Story Panel + + + + ); +}; + +const Wrapper = styled.div(({ theme }) => ({ + display: "flex", + flexDirection: "column", + overflow: "auto", + padding: theme.spacing.smallest +})); + +const Row = styled.div(({ theme }) => ({ + display: "flex", + alignItems: "center", + padding: theme.spacing.smallest, + "&:hover": { + backgroundColor: theme.bg[1] + }, + borderRadius: theme.radius.small, + minHeight: 28 +})); + +export default SettingsList; diff --git a/web/src/beta/features/PluginPlayground/Viewer/index.tsx b/web/src/beta/features/PluginPlayground/Viewer/index.tsx index 03be01e8d6..440d4435c4 100644 --- a/web/src/beta/features/PluginPlayground/Viewer/index.tsx +++ b/web/src/beta/features/PluginPlayground/Viewer/index.tsx @@ -2,15 +2,25 @@ import Visualizer from "@reearth/beta/features/Visualizer"; import { Layer, MapRef } from "@reearth/core"; import { ComponentProps, FC, MutableRefObject } from "react"; +import { Story } from "../../Visualizer/Crust/StoryPanel"; + import useHooks from "./hooks"; type Props = { layers: Layer[]; - widgets: ComponentProps["widgets"]; + showStoryPanel: boolean; + story: Story | undefined; visualizerRef: MutableRefObject; + widgets: ComponentProps["widgets"]; }; -const Viewer: FC = ({ layers, widgets, visualizerRef }) => { +const Viewer: FC = ({ + layers, + showStoryPanel, + story, + visualizerRef, + widgets +}) => { const { currentCamera, engineMeta, ready, setCurrentCamera, viewerProperty } = useHooks({ visualizerRef }); @@ -25,6 +35,8 @@ const Viewer: FC = ({ layers, widgets, visualizerRef }) => { currentCamera={currentCamera} onCameraChange={setCurrentCamera} widgets={widgets} + story={story} + showStoryPanel={showStoryPanel} /> ); }; diff --git a/web/src/beta/features/PluginPlayground/hooks.tsx b/web/src/beta/features/PluginPlayground/hooks.tsx index e7483d8308..90f65b9b4f 100644 --- a/web/src/beta/features/PluginPlayground/hooks.tsx +++ b/web/src/beta/features/PluginPlayground/hooks.tsx @@ -1,5 +1,5 @@ import { TabItem } from "@reearth/beta/lib/reearth-ui"; -import { Layer, MapRef } from "@reearth/core"; +import { MapRef } from "@reearth/core"; import { FC, useMemo, useRef, useState } from "react"; import Code from "./Code"; @@ -8,6 +8,7 @@ import LayerList from "./LayerList"; import { DEFAULT_LAYERS_PLUGIN_PLAYGROUND } from "./LayerList/constants"; import Plugins from "./Plugins"; import usePlugins from "./Plugins/usePlugins"; +import SettingsList from "./SettingsList"; import Viewer from "./Viewer"; export default () => { @@ -29,19 +30,46 @@ export default () => { sharedPlugin } = usePlugins(); - const { widgets, executeCode } = useCode({ + const { executeCode, infoboxBlocks, story, widgets } = useCode({ files: selectedPlugin.files }); - const [layers, setLayers] = useState( - DEFAULT_LAYERS_PLUGIN_PLAYGROUND + const [infoboxEnabled, setInfoboxEnabled] = useState(true); + const [selectedLayerId, setSelectedLayerId] = useState(""); + const [showStoryPanel, setShowStoryPanel] = useState(false); + const [visibleLayerIds, setVisibleLayerIds] = useState( + DEFAULT_LAYERS_PLUGIN_PLAYGROUND.map((l) => l.id) ); - const handleLayerVisibilityUpdate = (layerId: string, visible: boolean) => { - setLayers((prev) => - prev.map((layer) => - layer.id === layerId ? { ...layer, visible } : layer - ) + const layers = useMemo(() => { + return DEFAULT_LAYERS_PLUGIN_PLAYGROUND.map((layer) => { + return { + ...layer, + ...(infoboxEnabled + ? { + infobox: { + id: layer.id, + blocks: infoboxBlocks, + property: { + default: { + enabled: { + value: true + } + } + } + } + } + : {}), + visible: visibleLayerIds.includes(layer.id) + }; + }); + }, [infoboxEnabled, visibleLayerIds, infoboxBlocks]); + + const handleLayerVisibilityUpdate = (layerId: string) => { + setVisibleLayerIds((prev) => + prev.includes(layerId) + ? prev.filter((id) => id !== layerId) + : [...prev, layerId] ); }; @@ -55,19 +83,23 @@ export default () => { children: ( ) } ], - [layers, widgets] + [layers, showStoryPanel, story, widgets] ); const LayersPanel: FC = () => ( ); @@ -134,10 +166,20 @@ export default () => { [selectedFile, executeCode, updateFileSourceCode] ); + const SettingsPanel: FC = () => ( + + ); + return { - MainAreaTabs, LayersPanel, - SubRightAreaTabs, - RightAreaTabs + MainAreaTabs, + RightAreaTabs, + SettingsPanel, + SubRightAreaTabs }; }; diff --git a/web/src/beta/features/PluginPlayground/index.tsx b/web/src/beta/features/PluginPlayground/index.tsx index 146e6b3ccc..76a63a783d 100644 --- a/web/src/beta/features/PluginPlayground/index.tsx +++ b/web/src/beta/features/PluginPlayground/index.tsx @@ -5,8 +5,13 @@ import { FC } from "react"; import useHooks from "./hooks"; const PluginPlayground: FC = () => { - const { MainAreaTabs, LayersPanel, SubRightAreaTabs, RightAreaTabs } = - useHooks(); + const { + LayersPanel, + MainAreaTabs, + RightAreaTabs, + SettingsPanel, + SubRightAreaTabs + } = useHooks(); return ( @@ -23,6 +28,9 @@ const PluginPlayground: FC = () => { + + +