diff --git a/docs/ObservationPresets.MD b/docs/ObservationPresets.MD
new file mode 100644
index 000000000..a3a7f3ead
--- /dev/null
+++ b/docs/ObservationPresets.MD
@@ -0,0 +1,72 @@
+## Relationship between presets, fields, and tags
+
+### Tags:
+
+An `Observation` has a tags property.
+
+```ts
+type tags = {
+ [k: string]:
+ | boolean
+ | number
+ | string
+ | null
+ | (boolean | number | string | null)[];
+};
+```
+
+This is an open ended propery, that allows any key value pair descriptor to be associated with an observation. For example, any notes associated with a observation is simply saved as a tag, with the `key="notes"`
+
+### Presets:
+
+Presets have a set of predefined tags used to categorize an observation. These tags can have as little or as many descriptors, usually defined by how it is used.
+
+In the following example, there are 2 different types of tags, defined by the presets, both describing bridges.
+
+```ts
+// This tag simply defines the observation as being a bridge
+const tags = {
+ name: 'bridge',
+};
+
+// this tag defines the observation as an active suspension bridge over a river
+const tags = {
+ name: 'bridge',
+ bridgeType: 'suspension',
+ bodyOfWater: 'river',
+ isActive: true,
+};
+```
+
+In CoMapeo each preset represnts one type of observation (defined by the name tag). Preset also includes other meta data associated with an observation.
+
+### Fields:
+
+Fields are also saved to an observation as a tag. The difference is that they are editted by the user at the time of the observation. A `preset` has predefined `keys` and a predefined `values`, while a `tag` has predefined `keys` and user editted values. This is useful when there are descriptors that cannot be predetermined.
+
+Using the bridge example:
+
+```ts
+const tags = {
+ // this is predefined by the preset
+ type: 'bridge',
+ // this is a field, where the user was prompted to input the length
+ lengthInMeter: 35,
+};
+```
+
+### How to determine the fields associated with a preset
+
+`Presets` have a `fieldsId` property of type `string[]`. For each value in the array, there is an associated field with a matching docId.
+
+Each observation can have a several fields associated with it.
+
+Each field has a `type`, where `type: "text" | "number" | "selectOne" | "selectMultiple" | "UNRECOGNIZED";`. This defines how the field is presented to the user,and how the user inputs the value of the field. If the type is `text` or `number` the user inputs a string or number. If the type is `selectOne` or `selectMultiple` the user types is given a list of options to select from which determines the value of the field
+
+A field has a `tagKey` property. This defined the `key` of the tag saved in the observation
+
+### Flow for saving an observation
+
+1. User choses a preset. This preset has `tags` and `fieldIds`.
+2. Based on the `fieldIds` the user is prompted to fill in several forms. This provides an another `tags` object where the `keys=Field['tagKeys']` and the value is the value inputted by the user.
+3. The tags from the preset, and the tags from the fields are flattened into `tags` object of an observation.
diff --git a/docs/ObservationPresets.md b/docs/ObservationPresets.md
index d2fe830c2..a3a7f3ead 100644
--- a/docs/ObservationPresets.md
+++ b/docs/ObservationPresets.md
@@ -2,7 +2,7 @@
### Tags:
-An `Observation` has a `tags` property.
+An `Observation` has a tags property.
```ts
type tags = {
@@ -15,7 +15,7 @@ type tags = {
};
```
-This is an open-ended property that allows any key-value pair descriptor to be associated with an observation. For example, any notes associated with an observation is simply saved as a tag, with the `key="notes"`
+This is an open ended propery, that allows any key value pair descriptor to be associated with an observation. For example, any notes associated with a observation is simply saved as a tag, with the `key="notes"`
### Presets:
@@ -25,12 +25,12 @@ In the following example, there are 2 different types of tags, defined by the pr
```ts
// This tag simply defines the observation as being a bridge
-const tag = {
+const tags = {
name: 'bridge',
};
// this tag defines the observation as an active suspension bridge over a river
-const tag = {
+const tags = {
name: 'bridge',
bridgeType: 'suspension',
bodyOfWater: 'river',
@@ -38,11 +38,11 @@ const tag = {
};
```
-In CoMapeo each preset represents one type of observation (defined by the `name` tag). Preset also includes other metadata associated with an observation.
+In CoMapeo each preset represnts one type of observation (defined by the name tag). Preset also includes other meta data associated with an observation.
### Fields:
-Fields are also saved to an observation as a tag. The difference is that they are edited by the user at the time of the observation. A `preset` has predefined `keys` and a predefined `values`, while a `tag` has predefined `keys` and user-edited values. This is useful when there are descriptors that cannot be predetermined.
+Fields are also saved to an observation as a tag. The difference is that they are editted by the user at the time of the observation. A `preset` has predefined `keys` and a predefined `values`, while a `tag` has predefined `keys` and user editted values. This is useful when there are descriptors that cannot be predetermined.
Using the bridge example:
@@ -57,16 +57,16 @@ const tags = {
### How to determine the fields associated with a preset
-`Presets` have a `fieldsId` property of type `string[]`. For each value in the array, there is an associated field with a matching `docId`.
+`Presets` have a `fieldsId` property of type `string[]`. For each value in the array, there is an associated field with a matching docId.
Each observation can have a several fields associated with it.
-Each field has a `type`, where `type: "text" | "number" | "selectOne" | "selectMultiple" | "UNRECOGNIZED";`. This defines how the field is presented to the user, and how the user inputs the value of the field. If the type is `text` or `number` the user inputs a string or number. If the type is `selectOne` or `selectMultiple` the user is given a list of options to select from which determines the value of the field.
+Each field has a `type`, where `type: "text" | "number" | "selectOne" | "selectMultiple" | "UNRECOGNIZED";`. This defines how the field is presented to the user,and how the user inputs the value of the field. If the type is `text` or `number` the user inputs a string or number. If the type is `selectOne` or `selectMultiple` the user types is given a list of options to select from which determines the value of the field
-A field has a `tagKey` property. This defines the `key` of the tag saved in the observation.
+A field has a `tagKey` property. This defined the `key` of the tag saved in the observation
### Flow for saving an observation
-1. User chooses a preset. This preset has `tags` and `fieldIds`.
-2. Based on the `fieldIds` the user is prompted to fill in several forms. This provides another `tags` object where the `keys=Field['tagKeys']` and the value is the value inputted by the user.
-3. The tags from the preset and the tags from the fields are flattened into the `tags` object of an observation.
+1. User choses a preset. This preset has `tags` and `fieldIds`.
+2. Based on the `fieldIds` the user is prompted to fill in several forms. This provides an another `tags` object where the `keys=Field['tagKeys']` and the value is the value inputted by the user.
+3. The tags from the preset, and the tags from the fields are flattened into `tags` object of an observation.
diff --git a/messages/en.json b/messages/en.json
index 4b503feac..7cdad4981 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -350,6 +350,18 @@
"description": "Title of observation screen showing (non-editable) view of observation with map and answered questions",
"message": "Observation"
},
+ "screens.ObservationDetails.done": {
+ "description": "Button text when all questions are complete",
+ "message": "Done"
+ },
+ "screens.ObservationDetails.nextQuestion": {
+ "description": "Button text to navigate to next question",
+ "message": "Next"
+ },
+ "screens.ObservationDetails.title": {
+ "description": "Title of observation details screen showing question number and total",
+ "message": "Question {current} of {total}"
+ },
"screens.ObservationEdit.BottomSheet.addLabel": {
"description": "Label above keyboard that expands into bottom sheet of options to add (photo, details etc)",
"message": "Add…"
@@ -358,6 +370,10 @@
"description": "Placeholder for description/notes field",
"message": "What is happening here?"
},
+ "screens.ObservationEdit.ObservationEditView.detailsButton": {
+ "description": "Button label to add details",
+ "message": "Add Details"
+ },
"screens.ObservationEdit.ObservationEditView.photoButton": {
"description": "Button label for adding photo",
"message": "Add Photo"
diff --git a/package.json b/package.json
index 43520d664..fe3371f8f 100644
--- a/package.json
+++ b/package.json
@@ -30,7 +30,7 @@
"@formatjs/intl-pluralrules": "^5.2.4",
"@formatjs/intl-relativetimeformat": "^11.2.4",
"@gorhom/bottom-sheet": "^4.5.1",
- "@mapeo/ipc": "0.3.0",
+ "@mapeo/ipc": "^0.4.0",
"@osm_borders/maritime_10000m": "^1.1.0",
"@react-native-community/hooks": "^2.8.0",
"@react-native-community/netinfo": "11.1.0",
@@ -95,7 +95,7 @@
"@babel/preset-env": "^7.20.0",
"@babel/runtime": "^7.20.0",
"@formatjs/cli": "^6.2.0",
- "@mapeo/core": "9.0.0-alpha.7",
+ "@mapeo/core": "^9.0.0-alpha.8",
"@mapeo/schema": "3.0.0-next.15",
"@react-native-community/cli": "^12.3.6",
"@react-native/babel-preset": "^0.73.21",
diff --git a/scripts/build-backend.mjs b/scripts/build-backend.mjs
index 6a9a5d193..ed52cda0e 100755
--- a/scripts/build-backend.mjs
+++ b/scripts/build-backend.mjs
@@ -74,6 +74,7 @@ const KEEP_THESE = [
'loader.js',
// Static folders referenced by @mapeo/core code
'node_modules/@mapeo/core/drizzle',
+ 'node_modules/@mapeo/default-config'
];
for (const name of KEEP_THESE) {
diff --git a/src/backend/index.js b/src/backend/index.js
index 3609997a0..2c50554fb 100644
--- a/src/backend/index.js
+++ b/src/backend/index.js
@@ -10,6 +10,11 @@ const MIGRATIONS_FOLDER_PATH = new URL(
import.meta.url,
).pathname
+const DEFAULT_CONFIG_PATH = new URL(
+ './node_modules/@mapeo/default-config/dist/mapeo-default-config.mapeoconfig',
+ import.meta.url,
+).pathname
+
try {
const { values } = parseArgs({
options: {
@@ -35,6 +40,7 @@ try {
rootKey: Buffer.from(values.rootKey, 'hex'),
migrationsFolderPath: MIGRATIONS_FOLDER_PATH,
sharedStoragePath: values.sharedStoragePath,
+ defaultConfigPath: DEFAULT_CONFIG_PATH,
}).catch((err) => {
console.error('Server startup error:', err)
})
diff --git a/src/backend/package-lock.json b/src/backend/package-lock.json
index 7404de6c8..414f78949 100644
--- a/src/backend/package-lock.json
+++ b/src/backend/package-lock.json
@@ -10,8 +10,9 @@
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
- "@mapeo/core": "9.0.0-alpha.7",
- "@mapeo/ipc": "0.3.0",
+ "@mapeo/core": "^9.0.0-alpha.8",
+ "@mapeo/default-config": "^4.0.0-alpha.2",
+ "@mapeo/ipc": "^0.4.0",
"debug": "^4.3.4"
},
"devDependencies": {
@@ -332,24 +333,59 @@
}
},
"node_modules/@fastify/static": {
- "version": "6.12.0",
- "resolved": "https://registry.npmjs.org/@fastify/static/-/static-6.12.0.tgz",
- "integrity": "sha512-KK1B84E6QD/FcQWxDI2aiUCwHxMJBI1KeCUzm1BwYpPY1b742+jeKruGHP2uOluuM6OkBPI8CIANrXcCRtC2oQ==",
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/@fastify/static/-/static-7.0.3.tgz",
+ "integrity": "sha512-2tmTdF+uFCykasutaO6k4/wOt7eXyi7m3dGuCPo5micXzv0qt6ttb/nWnDYL/BlXjYGfp1JI4a1gyluTIylvQA==",
"dependencies": {
"@fastify/accept-negotiator": "^1.0.0",
"@fastify/send": "^2.0.0",
"content-disposition": "^0.5.3",
"fastify-plugin": "^4.0.0",
- "glob": "^8.0.1",
- "p-limit": "^3.1.0"
+ "fastq": "^1.17.0",
+ "glob": "^10.3.4"
+ }
+ },
+ "node_modules/@fastify/static/node_modules/glob": {
+ "version": "10.3.12",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz",
+ "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^2.3.6",
+ "minimatch": "^9.0.1",
+ "minipass": "^7.0.4",
+ "path-scurry": "^1.10.2"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@fastify/static/node_modules/minimatch": {
+ "version": "9.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
+ "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@fastify/type-provider-typebox": {
- "version": "3.5.0",
- "resolved": "https://registry.npmjs.org/@fastify/type-provider-typebox/-/type-provider-typebox-3.5.0.tgz",
- "integrity": "sha512-f48uGzvLflE/y4pvXOS8qjAC+mZmlqev9CPHnB8NDsBSL4EbeydO61IgPuzOkeNlAYeRP9Y56UOKj1XWFibgMw==",
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@fastify/type-provider-typebox/-/type-provider-typebox-4.0.0.tgz",
+ "integrity": "sha512-kTlN0saC/+xhcQPyBjb3YONQAMjiD/EHlCRjQjsr5E3NFjS5K8ZX5LGzXYDRjSa+sV4y8gTL5Q7FlObePv4iTA==",
"peerDependencies": {
- "@sinclair/typebox": ">=0.26 <=0.31"
+ "@sinclair/typebox": ">=0.26 <=0.32"
}
},
"node_modules/@humanwhocodes/config-array": {
@@ -482,16 +518,16 @@
}
},
"node_modules/@mapeo/core": {
- "version": "9.0.0-alpha.7",
- "resolved": "https://registry.npmjs.org/@mapeo/core/-/core-9.0.0-alpha.7.tgz",
- "integrity": "sha512-8DXZPKtMMVLTgZX3F8Eew3KnLB6bmf9v7uda9TvMX14YM9np4G9IiYafPS/X7kFNvr7h823wMzkRotNnHZ3byg==",
+ "version": "9.0.0-alpha.8",
+ "resolved": "https://registry.npmjs.org/@mapeo/core/-/core-9.0.0-alpha.8.tgz",
+ "integrity": "sha512-B+v5cgAsaUOtPbm6ObrgQbRMiaqa2cNYcsh3WwBocAC3WJAlZl7jCriNwu+dvGVdA9N+5orIV3PVLEYe5VhAUA==",
"hasInstallScript": true,
"dependencies": {
"@digidem/types": "^2.2.0",
"@electron/asar": "^3.2.8",
"@fastify/error": "^3.4.1",
- "@fastify/static": "^6.12.0",
- "@fastify/type-provider-typebox": "^3.3.0",
+ "@fastify/static": "^7.0.3",
+ "@fastify/type-provider-typebox": "^4.0.0",
"@hyperswarm/secret-stream": "^6.1.2",
"@mapeo/crypto": "1.0.0-alpha.10",
"@mapeo/schema": "3.0.0-next.15",
@@ -506,12 +542,13 @@
"debug": "^4.3.4",
"drizzle-orm": "^0.30.8",
"fastify": ">= 4",
- "fastify-plugin": "^4.5.0",
+ "fastify-plugin": "^4.5.1",
"hyperblobs": "2.3.0",
"hypercore": "10.17.0",
"hypercore-crypto": "3.4.0",
"hyperdrive": "11.5.3",
"hyperswarm": "4.4.1",
+ "json-stable-stringify": "^1.1.1",
"magic-bytes.js": "^1.10.0",
"map-obj": "^5.0.2",
"mime": "^4.0.1",
@@ -627,10 +664,15 @@
"z32": "^1.0.0"
}
},
+ "node_modules/@mapeo/default-config": {
+ "version": "4.0.0-alpha.2",
+ "resolved": "https://registry.npmjs.org/@mapeo/default-config/-/default-config-4.0.0-alpha.2.tgz",
+ "integrity": "sha512-3CxFRO8EfhoAzzYmiSS44eEwt3oLqveNTUNBsOqPc/OWhoniYfTzVrSo676pV4ThIFDH884K9e5cwgT59hErAQ=="
+ },
"node_modules/@mapeo/ipc": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/@mapeo/ipc/-/ipc-0.3.0.tgz",
- "integrity": "sha512-8OdARTEBgCFCXrcJqwPuz397i10MiCIGG1Y9SSZ7myzN2o72lbuVAu7SX/D2gbHrVVD2tuia9EwaDBVsznas2w==",
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@mapeo/ipc/-/ipc-0.4.0.tgz",
+ "integrity": "sha512-Iqf2p/KDwZghwSNtKXWDi8+CR9vi2ovO68AKGVFUtKRbBDvzqsE9e1bVtyYwZIQInwIFUVOI2X/83krHi1g0og==",
"dependencies": {
"eventemitter3": "^5.0.1",
"p-defer": "^4.0.0",
@@ -640,7 +682,7 @@
"node": ">=18.17.1"
},
"peerDependencies": {
- "@mapeo/core": "9.0.0-alpha.7"
+ "@mapeo/core": "9.0.0-alpha.8"
}
},
"node_modules/@mapeo/schema": {
@@ -1653,6 +1695,24 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/call-bind": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
+ "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "set-function-length": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -2559,9 +2619,9 @@
"integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ=="
},
"node_modules/fastq": {
- "version": "1.15.0",
- "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
- "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
+ "version": "1.17.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
+ "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
"dependencies": {
"reusify": "^1.0.4"
}
@@ -2832,6 +2892,7 @@
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz",
"integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==",
+ "dev": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@@ -3366,6 +3427,11 @@
"node": ">=8"
}
},
+ "node_modules/isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="
+ },
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -3421,11 +3487,17 @@
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
},
"node_modules/json-stable-stringify": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.2.tgz",
- "integrity": "sha512-eunSSaEnxV12z+Z73y/j5N37/In40GK4GmsSy+tEHJMxknvqnA7/djeYtAgW0GsWHUfg+847WJjKaEylk2y09g==",
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.1.1.tgz",
+ "integrity": "sha512-SU/971Kt5qVQfJpyDveVhQ/vya+5hvrjClFOcr8c0Fq5aODJjMwutrOfCU+eCnVD5gpx1Q3fEqkyom77zH1iIg==",
"dependencies": {
- "jsonify": "^0.0.1"
+ "call-bind": "^1.0.5",
+ "isarray": "^2.0.5",
+ "jsonify": "^0.0.1",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -3640,6 +3712,7 @@
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz",
"integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==",
+ "dev": true,
"dependencies": {
"brace-expansion": "^2.0.1"
},
@@ -3913,6 +3986,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
"dependencies": {
"yocto-queue": "^0.1.0"
},
@@ -4692,6 +4766,22 @@
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz",
"integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ=="
},
+ "node_modules/set-function-length": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -5564,6 +5654,7 @@
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
"engines": {
"node": ">=10"
},
@@ -5818,22 +5909,44 @@
}
},
"@fastify/static": {
- "version": "6.12.0",
- "resolved": "https://registry.npmjs.org/@fastify/static/-/static-6.12.0.tgz",
- "integrity": "sha512-KK1B84E6QD/FcQWxDI2aiUCwHxMJBI1KeCUzm1BwYpPY1b742+jeKruGHP2uOluuM6OkBPI8CIANrXcCRtC2oQ==",
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/@fastify/static/-/static-7.0.3.tgz",
+ "integrity": "sha512-2tmTdF+uFCykasutaO6k4/wOt7eXyi7m3dGuCPo5micXzv0qt6ttb/nWnDYL/BlXjYGfp1JI4a1gyluTIylvQA==",
"requires": {
"@fastify/accept-negotiator": "^1.0.0",
"@fastify/send": "^2.0.0",
"content-disposition": "^0.5.3",
"fastify-plugin": "^4.0.0",
- "glob": "^8.0.1",
- "p-limit": "^3.1.0"
+ "fastq": "^1.17.0",
+ "glob": "^10.3.4"
+ },
+ "dependencies": {
+ "glob": {
+ "version": "10.3.12",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz",
+ "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==",
+ "requires": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^2.3.6",
+ "minimatch": "^9.0.1",
+ "minipass": "^7.0.4",
+ "path-scurry": "^1.10.2"
+ }
+ },
+ "minimatch": {
+ "version": "9.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
+ "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
+ "requires": {
+ "brace-expansion": "^2.0.1"
+ }
+ }
}
},
"@fastify/type-provider-typebox": {
- "version": "3.5.0",
- "resolved": "https://registry.npmjs.org/@fastify/type-provider-typebox/-/type-provider-typebox-3.5.0.tgz",
- "integrity": "sha512-f48uGzvLflE/y4pvXOS8qjAC+mZmlqev9CPHnB8NDsBSL4EbeydO61IgPuzOkeNlAYeRP9Y56UOKj1XWFibgMw==",
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@fastify/type-provider-typebox/-/type-provider-typebox-4.0.0.tgz",
+ "integrity": "sha512-kTlN0saC/+xhcQPyBjb3YONQAMjiD/EHlCRjQjsr5E3NFjS5K8ZX5LGzXYDRjSa+sV4y8gTL5Q7FlObePv4iTA==",
"requires": {}
},
"@humanwhocodes/config-array": {
@@ -5949,15 +6062,15 @@
"integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA=="
},
"@mapeo/core": {
- "version": "9.0.0-alpha.7",
- "resolved": "https://registry.npmjs.org/@mapeo/core/-/core-9.0.0-alpha.7.tgz",
- "integrity": "sha512-8DXZPKtMMVLTgZX3F8Eew3KnLB6bmf9v7uda9TvMX14YM9np4G9IiYafPS/X7kFNvr7h823wMzkRotNnHZ3byg==",
+ "version": "9.0.0-alpha.8",
+ "resolved": "https://registry.npmjs.org/@mapeo/core/-/core-9.0.0-alpha.8.tgz",
+ "integrity": "sha512-B+v5cgAsaUOtPbm6ObrgQbRMiaqa2cNYcsh3WwBocAC3WJAlZl7jCriNwu+dvGVdA9N+5orIV3PVLEYe5VhAUA==",
"requires": {
"@digidem/types": "^2.2.0",
"@electron/asar": "^3.2.8",
"@fastify/error": "^3.4.1",
- "@fastify/static": "^6.12.0",
- "@fastify/type-provider-typebox": "^3.3.0",
+ "@fastify/static": "^7.0.3",
+ "@fastify/type-provider-typebox": "^4.0.0",
"@hyperswarm/secret-stream": "^6.1.2",
"@mapeo/crypto": "1.0.0-alpha.10",
"@mapeo/schema": "3.0.0-next.15",
@@ -5972,12 +6085,13 @@
"debug": "^4.3.4",
"drizzle-orm": "^0.30.8",
"fastify": ">= 4",
- "fastify-plugin": "^4.5.0",
+ "fastify-plugin": "^4.5.1",
"hyperblobs": "2.3.0",
"hypercore": "10.17.0",
"hypercore-crypto": "3.4.0",
"hyperdrive": "11.5.3",
"hyperswarm": "4.4.1",
+ "json-stable-stringify": "^1.1.1",
"magic-bytes.js": "^1.10.0",
"map-obj": "^5.0.2",
"mime": "^4.0.1",
@@ -6083,10 +6197,15 @@
"z32": "^1.0.0"
}
},
+ "@mapeo/default-config": {
+ "version": "4.0.0-alpha.2",
+ "resolved": "https://registry.npmjs.org/@mapeo/default-config/-/default-config-4.0.0-alpha.2.tgz",
+ "integrity": "sha512-3CxFRO8EfhoAzzYmiSS44eEwt3oLqveNTUNBsOqPc/OWhoniYfTzVrSo676pV4ThIFDH884K9e5cwgT59hErAQ=="
+ },
"@mapeo/ipc": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/@mapeo/ipc/-/ipc-0.3.0.tgz",
- "integrity": "sha512-8OdARTEBgCFCXrcJqwPuz397i10MiCIGG1Y9SSZ7myzN2o72lbuVAu7SX/D2gbHrVVD2tuia9EwaDBVsznas2w==",
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@mapeo/ipc/-/ipc-0.4.0.tgz",
+ "integrity": "sha512-Iqf2p/KDwZghwSNtKXWDi8+CR9vi2ovO68AKGVFUtKRbBDvzqsE9e1bVtyYwZIQInwIFUVOI2X/83krHi1g0og==",
"requires": {
"eventemitter3": "^5.0.1",
"p-defer": "^4.0.0",
@@ -6772,6 +6891,18 @@
"integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==",
"dev": true
},
+ "call-bind": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
+ "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
+ "requires": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "set-function-length": "^1.2.1"
+ }
+ },
"callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -7419,9 +7550,9 @@
"integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ=="
},
"fastq": {
- "version": "1.15.0",
- "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
- "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
+ "version": "1.17.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
+ "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
"requires": {
"reusify": "^1.0.4"
}
@@ -7629,6 +7760,7 @@
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz",
"integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==",
+ "dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@@ -8027,6 +8159,11 @@
"is-docker": "^2.0.0"
}
},
+ "isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="
+ },
"isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -8071,11 +8208,14 @@
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
},
"json-stable-stringify": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.2.tgz",
- "integrity": "sha512-eunSSaEnxV12z+Z73y/j5N37/In40GK4GmsSy+tEHJMxknvqnA7/djeYtAgW0GsWHUfg+847WJjKaEylk2y09g==",
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.1.1.tgz",
+ "integrity": "sha512-SU/971Kt5qVQfJpyDveVhQ/vya+5hvrjClFOcr8c0Fq5aODJjMwutrOfCU+eCnVD5gpx1Q3fEqkyom77zH1iIg==",
"requires": {
- "jsonify": "^0.0.1"
+ "call-bind": "^1.0.5",
+ "isarray": "^2.0.5",
+ "jsonify": "^0.0.1",
+ "object-keys": "^1.1.1"
}
},
"json-stable-stringify-without-jsonify": {
@@ -8237,6 +8377,7 @@
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz",
"integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==",
+ "dev": true,
"requires": {
"brace-expansion": "^2.0.1"
}
@@ -8450,6 +8591,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
"requires": {
"yocto-queue": "^0.1.0"
}
@@ -9019,6 +9161,19 @@
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz",
"integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ=="
},
+ "set-function-length": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+ "requires": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2"
+ }
+ },
"setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -9705,7 +9860,8 @@
"yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
- "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true
},
"z32": {
"version": "1.0.1",
diff --git a/src/backend/package.json b/src/backend/package.json
index 1d0613228..0a300ed6a 100644
--- a/src/backend/package.json
+++ b/src/backend/package.json
@@ -13,8 +13,9 @@
"author": "Digital Democracy",
"license": "MIT",
"dependencies": {
- "@mapeo/core": "9.0.0-alpha.7",
- "@mapeo/ipc": "0.3.0",
+ "@mapeo/core": "^9.0.0-alpha.8",
+ "@mapeo/default-config": "^4.0.0-alpha.2",
+ "@mapeo/ipc": "^0.4.0",
"debug": "^4.3.4"
},
"devDependencies": {
diff --git a/src/backend/src/app.js b/src/backend/src/app.js
index 780999e13..66b7564d7 100644
--- a/src/backend/src/app.js
+++ b/src/backend/src/app.js
@@ -17,6 +17,7 @@ const DB_DIR_NAME = 'sqlite-dbs'
const CORE_STORAGE_DIR_NAME = 'core-storage'
const log = debug('mapeo:app')
+debug.enable('*')
// Set these up as soon as possible (e.g. before the init function)
const serverStatus = new ServerStatus()
@@ -47,13 +48,14 @@ process.on('exit', (code) => {
* @param {Buffer} options.rootKey
* @param {string} options.migrationsFolderPath
* @param {string} options.sharedStoragePath Path to app-specific external file storage folder
- *
+ * @param {string} [options.defaultConfigPath]
*/
export async function init({
version,
rootKey,
migrationsFolderPath,
sharedStoragePath,
+ defaultConfigPath,
}) {
log('Starting app...')
log(`Device version is ${version}`)
@@ -75,6 +77,7 @@ export async function init({
clientMigrationsFolder: join(migrationsFolderPath, 'client'),
projectMigrationsFolder: join(migrationsFolderPath, 'project'),
fastify,
+ defaultConfigPath,
})
// Don't await, methods that use the server will await this internally
diff --git a/src/frontend/Navigation/ScreenGroups/AppScreens.tsx b/src/frontend/Navigation/ScreenGroups/AppScreens.tsx
index 93fd8c817..3455726d3 100644
--- a/src/frontend/Navigation/ScreenGroups/AppScreens.tsx
+++ b/src/frontend/Navigation/ScreenGroups/AppScreens.tsx
@@ -46,6 +46,7 @@ import {
EditScreen as DeviceNameEditScreen,
createNavigationOptions as createDeviceNameEditNavOptions,
} from '../../screens/Settings/ProjectSettings/DeviceName/EditScreen';
+import {ObservationFields} from '../../screens/ObservationFields';
import {
GpsModal,
createNavigationOptions as createGpsModalNavigationOptions,
@@ -97,6 +98,7 @@ export type AppList = {
ObservationList: undefined;
Observation: {observationId: string};
ObservationEdit: {observationId?: string} | undefined;
+ ObservationFields: {question: number};
ManualGpsScreen: undefined;
ObservationDetails: {question: number};
LeaveProjectScreen: undefined;
@@ -355,6 +357,7 @@ export const createDefaultScreenGroup = (
component={DeviceNameEditScreen}
options={createDeviceNameEditNavOptions({intl})}
/>
+
= (
});
return;
},
- updatePreset: ({tags, fieldIds}) => {
+ updatePreset: ({tags, fieldIds, name}) => {
const prevValue = get().value;
+ // We want the name to overwrite the tags
+ const tagsWithName = {...tags, name};
if (!prevValue) {
set({
value: {
refs: [],
- tags: tags,
+ tags: tagsWithName,
metadata: {},
attachments: [],
},
@@ -140,7 +142,7 @@ const draftObservationSlice: StateCreator = (
value: {
...prevValue,
tags: {
- ...tags,
+ ...tagsWithName,
...savedFieldTags,
...(prevValue.tags.notes ? {notes: prevValue.tags.notes} : {}),
},
diff --git a/src/frontend/hooks/server/presets.ts b/src/frontend/hooks/server/presets.ts
index f6c8f4e67..6e2bfc756 100644
--- a/src/frontend/hooks/server/presets.ts
+++ b/src/frontend/hooks/server/presets.ts
@@ -16,10 +16,12 @@ export function usePresetsQuery() {
queryFn: async () => {
if (!project) throw new Error('Project instance does not exist');
const presets = await project.preset.getMany();
- if (presets.length === 0) {
- await Promise.all(MockPreset.map(val => project.preset.create(val)));
- return await project.preset.getMany();
- }
+ // if (presets.length === 0) {
+ // await Promise.all([
+ // ...MockPreset.map(val => project.preset.create(val)),
+ // ]);
+ // return await project.preset.getMany();
+ // }
return presets;
},
});
diff --git a/src/frontend/mockdata.ts b/src/frontend/mockdata.ts
index eb6f948a0..dcee085f2 100644
--- a/src/frontend/mockdata.ts
+++ b/src/frontend/mockdata.ts
@@ -1,5 +1,3 @@
-// @ts-check
-
import {FieldValue, Observation, PresetValue} from '@mapeo/schema';
export const mockObservations: Observation[] = [
diff --git a/src/frontend/screens/ObservationEdit/index.tsx b/src/frontend/screens/ObservationEdit/index.tsx
index 19ac7faa5..580b7ae58 100644
--- a/src/frontend/screens/ObservationEdit/index.tsx
+++ b/src/frontend/screens/ObservationEdit/index.tsx
@@ -12,6 +12,8 @@ import {PresetView} from './PresetView';
import {useBottomSheetModal} from '../../sharedComponents/BottomSheetModal';
import {ErrorModal} from '../../sharedComponents/ErrorModal';
import {SaveButton} from './SaveButton';
+import {useDraftObservation} from '../../hooks/useDraftObservation';
+import {DetailsIcon} from '../../sharedComponents/icons';
const m = defineMessages({
editTitle: {
@@ -29,6 +31,11 @@ const m = defineMessages({
defaultMessage: 'Add Photo',
description: 'Button label for adding photo',
},
+ detailsButton: {
+ id: 'screens.ObservationEdit.ObservationEditView.detailsButton',
+ defaultMessage: 'Add Details',
+ description: 'Button label to add details',
+ },
});
export const ObservationEdit: NativeNavigationComponent<'ObservationEdit'> & {
@@ -37,7 +44,8 @@ export const ObservationEdit: NativeNavigationComponent<'ObservationEdit'> & {
const observationId = usePersistedDraftObservation(
store => store.observationId,
);
-
+ const {usePreset} = useDraftObservation();
+ const preset = usePreset();
const isNew = !observationId;
const {formatMessage: t} = useIntl();
const {openSheet, sheetRef, isOpen, closeSheet} = useBottomSheetModal({
@@ -57,7 +65,7 @@ export const ObservationEdit: NativeNavigationComponent<'ObservationEdit'> & {
}, [navigation]);
const handleDetailsPress = React.useCallback(() => {
- navigation.navigate('ObservationDetails', {question: 1});
+ navigation.navigate('ObservationFields', {question: 1});
}, [navigation]);
const bottomSheetItems = [
@@ -67,14 +75,14 @@ export const ObservationEdit: NativeNavigationComponent<'ObservationEdit'> & {
onPress: handleCameraPress,
},
];
- // if (preset && preset.fields && preset.fields.length) {
- // // Only show the option to add details if preset fields are defined.
- // bottomSheetItems.push({
- // icon: ,
- // label: t(m.detailsButton),
- // onPress: handleDetailsPress,
- // });
- // }
+ if (preset && preset.fieldIds.length) {
+ // Only show the option to add details if preset fields are defined.
+ bottomSheetItems.push({
+ icon: ,
+ label: t(m.detailsButton),
+ onPress: handleDetailsPress,
+ });
+ }
return (
diff --git a/src/frontend/screens/ObservationFields/Question.tsx b/src/frontend/screens/ObservationFields/Question.tsx
new file mode 100644
index 000000000..d93a3f431
--- /dev/null
+++ b/src/frontend/screens/ObservationFields/Question.tsx
@@ -0,0 +1,26 @@
+import React from 'react';
+
+import {SelectOne} from './SelectOne';
+import {SelectMultiple} from './SelectMultiple';
+import {TextArea} from './TextArea';
+import {Field} from '@mapeo/schema';
+import {
+ SelectMultipleField,
+ SelectOneField,
+} from '../../sharedTypes/PresetTypes';
+
+export type QuestionProps = {
+ field: Field;
+};
+
+export const Question = ({field}: QuestionProps) => {
+ if (field.type === 'selectOne' && Array.isArray(field.options)) {
+ return ;
+ }
+
+ if (field.type === 'selectMultiple' && Array.isArray(field.options)) {
+ return ;
+ }
+
+ return ;
+};
diff --git a/src/frontend/screens/ObservationFields/QuestionLabel.tsx b/src/frontend/screens/ObservationFields/QuestionLabel.tsx
new file mode 100644
index 000000000..88231037d
--- /dev/null
+++ b/src/frontend/screens/ObservationFields/QuestionLabel.tsx
@@ -0,0 +1,40 @@
+import * as React from 'react';
+import {View, StyleSheet} from 'react-native';
+import {FormattedFieldProp} from '../../sharedComponents/FormattedData';
+import {Text} from '../../sharedComponents/Text';
+import {Field} from '@mapeo/schema';
+
+interface Props {
+ field: Field;
+}
+
+export const QuestionLabel = ({field}: Props) => {
+ const hint = ;
+ return (
+
+
+
+
+ {hint ? {hint} : null}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ labelContainer: {
+ flex: 0,
+ padding: 20,
+ borderBottomWidth: 2,
+ borderColor: '#F3F3F3',
+ },
+ label: {
+ fontSize: 20,
+ color: 'black',
+ fontWeight: '700',
+ },
+ hint: {
+ fontSize: 16,
+ color: '#666666',
+ fontWeight: '500',
+ },
+});
diff --git a/src/frontend/screens/ObservationFields/SelectMultiple.tsx b/src/frontend/screens/ObservationFields/SelectMultiple.tsx
new file mode 100644
index 000000000..c96bdd2ff
--- /dev/null
+++ b/src/frontend/screens/ObservationFields/SelectMultiple.tsx
@@ -0,0 +1,101 @@
+import * as React from 'react';
+import {View, StyleSheet} from 'react-native';
+import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
+
+import {Text} from '../../sharedComponents/Text';
+import {TouchableNativeFeedback} from '../../sharedComponents/Touchables';
+import {VERY_LIGHT_BLUE} from '../../lib/styles';
+import {QuestionLabel} from './QuestionLabel';
+import {convertSelectOptionsToLabeled} from '../../lib/utils';
+
+import type {QuestionProps} from './Question';
+import {SelectMultipleField} from '../../sharedTypes/PresetTypes';
+import {ViewStyleProp} from '../../sharedTypes';
+import {usePersistedDraftObservation} from '../../hooks/persistedState/usePersistedDraftObservation';
+import {useDraftObservation} from '../../hooks/useDraftObservation';
+import {Observation} from '@mapeo/schema';
+
+interface Props extends QuestionProps {
+ field: SelectMultipleField;
+}
+
+type CheckItemProps = {
+ checked: boolean;
+ onPress: () => any;
+ label: string;
+ style: ViewStyleProp;
+};
+
+const CheckItem = ({checked, onPress, label, style}: CheckItemProps) => (
+
+
+
+ {label}
+
+
+);
+
+export const SelectMultiple = React.memo(({field}) => {
+ const tags = usePersistedDraftObservation(val => val.value?.tags);
+ const valueAsArray = toArray(tags ? tags[field.tagKey] : undefined);
+ const {updateTags} = useDraftObservation();
+
+ const handleChange = (
+ itemValue: SelectMultipleField['options'][0]['value'],
+ ) => {
+ const updatedValue = valueAsArray.includes(itemValue)
+ ? valueAsArray.filter(d => d !== itemValue)
+ : [...valueAsArray, itemValue];
+ updateTags(field.tagKey, updatedValue);
+ };
+
+ return (
+ <>
+
+ {convertSelectOptionsToLabeled(field.options).map((item, index) => (
+ handleChange(item.value)}
+ checked={valueAsArray.includes(item.value)}
+ label={item.label}
+ style={[styles.radioContainer, index === 0 ? styles.noBorder : {}]}
+ />
+ ))}
+ >
+ );
+});
+
+function toArray(value?: Observation['tags'][0]) {
+ // null or undefined
+ if (!value) {
+ return [];
+ }
+ return Array.isArray(value) ? value : [value];
+}
+
+const styles = StyleSheet.create({
+ radioContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingVertical: 20,
+ marginHorizontal: 20,
+ borderTopWidth: 1,
+ borderColor: '#F3F3F3',
+ },
+ noBorder: {
+ borderTopWidth: 0,
+ },
+ itemLabel: {
+ fontSize: 18,
+ lineHeight: 24,
+ marginLeft: 20,
+ flex: 1,
+ color: 'black',
+ fontWeight: '700',
+ },
+});
diff --git a/src/frontend/screens/ObservationFields/SelectOne.tsx b/src/frontend/screens/ObservationFields/SelectOne.tsx
new file mode 100644
index 000000000..591527eee
--- /dev/null
+++ b/src/frontend/screens/ObservationFields/SelectOne.tsx
@@ -0,0 +1,88 @@
+import * as React from 'react';
+import {View, StyleSheet} from 'react-native';
+import {Text} from '../../sharedComponents/Text';
+import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
+import {useIntl} from 'react-intl';
+
+import {TouchableNativeFeedback} from '../../sharedComponents/Touchables';
+import {VERY_LIGHT_BLUE} from '../../lib/styles';
+import {QuestionLabel} from './QuestionLabel';
+import {convertSelectOptionsToLabeled} from '../../lib/utils';
+
+import type {QuestionProps} from './Question';
+import {ViewStyleProp} from '../../sharedTypes';
+import {useDraftObservation} from '../../hooks/useDraftObservation';
+import {usePersistedDraftObservation} from '../../hooks/persistedState/usePersistedDraftObservation';
+import {SelectOneField} from '../../sharedTypes/PresetTypes';
+
+interface Props extends QuestionProps {
+ field: SelectOneField;
+}
+
+type RadioItemProps = {
+ checked: boolean;
+ onPress: () => any;
+ label: string;
+ style: ViewStyleProp;
+};
+
+const RadioItem = ({checked, onPress, label, style}: RadioItemProps) => (
+
+
+
+ {label}
+
+
+);
+
+export const SelectOne = React.memo(({field}) => {
+ const {formatMessage: t} = useIntl();
+
+ const {updateTags} = useDraftObservation();
+ const tags = usePersistedDraftObservation(store => store.value?.tags);
+
+ return (
+ <>
+
+ {convertSelectOptionsToLabeled(field.options).map((item, index) => (
+ updateTags(field.tagKey, item.value)}
+ checked={tags && item.value === tags[field.tagKey] ? true : false}
+ label={t({
+ id: `fields.${field.docId}.options.${JSON.stringify(item.value)}`,
+ defaultMessage: item.label,
+ })}
+ style={[styles.radioContainer, index === 0 ? styles.noBorder : {}]}
+ />
+ ))}
+ >
+ );
+});
+
+const styles = StyleSheet.create({
+ radioContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingVertical: 20,
+ marginHorizontal: 20,
+ borderTopWidth: 1,
+ borderColor: '#F3F3F3',
+ },
+ noBorder: {
+ borderTopWidth: 0,
+ },
+ itemLabel: {
+ fontSize: 18,
+ lineHeight: 24,
+ marginLeft: 20,
+ flex: 1,
+ color: 'black',
+ fontWeight: '700',
+ },
+});
diff --git a/src/frontend/screens/ObservationFields/TextArea.tsx b/src/frontend/screens/ObservationFields/TextArea.tsx
new file mode 100644
index 000000000..5a993cc68
--- /dev/null
+++ b/src/frontend/screens/ObservationFields/TextArea.tsx
@@ -0,0 +1,41 @@
+import * as React from 'react';
+import {StyleSheet, TextInput} from 'react-native';
+import {QuestionLabel} from './QuestionLabel';
+import {Field} from '@mapeo/schema';
+import {usePersistedDraftObservation} from '../../hooks/persistedState/usePersistedDraftObservation';
+import {useDraftObservation} from '../../hooks/useDraftObservation';
+
+export const TextArea = React.memo<{field: Field}>(({field}) => {
+ const tags = usePersistedDraftObservation(store => store.value?.tags);
+ const {updateTags} = useDraftObservation();
+ const value = tags ? tags[field.tagKey] : '';
+ return (
+
+
+ updateTags(field.tagKey, newVal)}
+ style={styles.textInput}
+ underlineColorAndroid="transparent"
+ multiline
+ scrollEnabled={false}
+ textContentType="none"
+ autoFocus
+ />
+
+ );
+});
+
+const styles = StyleSheet.create({
+ textInput: {
+ flex: 1,
+ minHeight: 150,
+ fontSize: 20,
+ padding: 20,
+ marginBottom: 20,
+ color: 'black',
+ alignItems: 'flex-start',
+ justifyContent: 'flex-start',
+ textAlignVertical: 'top',
+ },
+});
diff --git a/src/frontend/screens/ObservationFields/index.tsx b/src/frontend/screens/ObservationFields/index.tsx
new file mode 100644
index 000000000..ba46c311e
--- /dev/null
+++ b/src/frontend/screens/ObservationFields/index.tsx
@@ -0,0 +1,167 @@
+import React from 'react';
+import {StyleSheet, Platform, ScrollView} from 'react-native';
+import {defineMessages, FormattedMessage, useIntl} from 'react-intl';
+
+import {Text} from '../../sharedComponents/Text';
+import {TextButton} from '../../sharedComponents/TextButton';
+import {Question} from './Question';
+import {NativeRootNavigationProps} from '../../sharedTypes';
+import {useNavigationFromRoot} from '../../hooks/useNavigationWithTypes';
+import {CustomHeaderLeft} from '../../sharedComponents/CustomHeaderLeft';
+
+import {Loading} from '../../sharedComponents/Loading';
+import {useFieldsQuery} from '../../hooks/server/fields';
+import {useDraftObservation} from '../../hooks/useDraftObservation';
+
+const m = defineMessages({
+ nextQuestion: {
+ id: 'screens.ObservationDetails.nextQuestion',
+ defaultMessage: 'Next',
+ description: 'Button text to navigate to next question',
+ },
+ done: {
+ id: 'screens.ObservationDetails.done',
+ defaultMessage: 'Done',
+ description: 'Button text when all questions are complete',
+ },
+ title: {
+ id: 'screens.ObservationDetails.title',
+ defaultMessage: 'Question {current} of {total}',
+ description:
+ 'Title of observation details screen showing question number and total',
+ },
+});
+
+export const ObservationFields = ({
+ navigation,
+ route,
+}: NativeRootNavigationProps<'ObservationFields'>) => {
+ const {usePreset} = useDraftObservation();
+ const preset = usePreset();
+ const current = route.params.question;
+ const fields = useFieldsQuery();
+
+ const onBackPress = React.useCallback(() => {
+ if (current === 1) {
+ navigation.navigate('ObservationEdit');
+ return;
+ }
+
+ navigation.navigate('ObservationFields', {
+ question: current - 1,
+ });
+ }, [current, navigation]);
+
+ React.useLayoutEffect(() => {
+ navigation.setOptions({
+ headerLeft: props => (
+
+ ),
+ headerTitle: () => ,
+ headerRight: () => ,
+ });
+ }, [navigation, current, onBackPress]);
+
+ // if (
+ // !preset ||
+ // preset.fieldIds.length < 1 ||
+ // current > preset.fieldIds.length
+ // ) {
+ // navigation.pop(current);
+ // return null;
+ // }
+
+ if (fields.isLoading) {
+ return ;
+ }
+
+ if (fields.isError) {
+ return null;
+ }
+
+ const fieldId = preset?.fieldIds[current - 1];
+ const field = fields.data?.find(val => val.docId === fieldId);
+
+ console.log({field: fields.data});
+
+ if (!field) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+};
+
+const DetailsHeaderRight = ({questionNumber}: {questionNumber: number}) => {
+ const {formatMessage: t} = useIntl();
+ const navigation = useNavigationFromRoot();
+ const {usePreset} = useDraftObservation();
+ const preset = usePreset();
+
+ const isLastQuestion =
+ questionNumber >= (preset ? preset.fieldIds.length : 0);
+ const buttonText = isLastQuestion ? t(m.done) : t(m.nextQuestion);
+
+ const onPress = () =>
+ isLastQuestion
+ ? navigation.navigate('ObservationEdit')
+ : navigation.navigate('ObservationFields', {
+ question: questionNumber + 1,
+ });
+
+ return (
+
+ );
+};
+
+const DetailsTitle = ({questionNumber}: {questionNumber: number}) => {
+ const {usePreset} = useDraftObservation();
+ const preset = usePreset();
+
+ return (
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ title: {
+ ...Platform.select({
+ ios: {
+ fontSize: 17,
+ fontWeight: '600',
+ color: 'rgba(0, 0, 0, .9)',
+ marginRight: 16,
+ },
+ android: {
+ fontSize: 20,
+ fontWeight: '500',
+ color: 'rgba(0, 0, 0, .9)',
+ marginRight: 16,
+ },
+ default: {
+ fontSize: 18,
+ fontWeight: '400',
+ color: '#3c4043',
+ },
+ }),
+ },
+ headerButton: {
+ paddingHorizontal: 20,
+ height: 60,
+ },
+});
diff --git a/src/frontend/screens/PresetChooser.tsx b/src/frontend/screens/PresetChooser.tsx
index 4b65f427f..bb493fb56 100644
--- a/src/frontend/screens/PresetChooser.tsx
+++ b/src/frontend/screens/PresetChooser.tsx
@@ -42,6 +42,8 @@ export const PresetChooser: NativeNavigationComponent<'PresetChooser'> = ({
const routes = state.routes;
const prevRouteNameInStack = routes[currentIndex - 1]?.name;
+ console.log({presets});
+
React.useLayoutEffect(() => {
navigation.setOptions({
headerLeft: props =>