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