diff --git a/blueprints/Missions/Reference-Mission/config.reference-mission.json b/blueprints/Missions/Reference-Mission/config.reference-mission.json index 15869772d9..5faf70570a 100644 --- a/blueprints/Missions/Reference-Mission/config.reference-mission.json +++ b/blueprints/Missions/Reference-Mission/config.reference-mission.json @@ -58,6 +58,7 @@ "screenshot": true, "fullscreen": true, "help": true, + "showAttributionsOnMap": true, "logourl": "", "helpurl": "https://nasa-ammos.github.io/MMGIS/", "aboutModalContent": "Welcome to the **MMGIS Reference Mission**.\n\nThis mission demonstrates the full capabilities of the Multi-Mission Geographic Information System." @@ -3731,6 +3732,48 @@ }, "description": "EMIT L2B Methane Enhancement Data 60 m V002 - STAC COG collection. Requires WITH_STAC=true and WITH_TITILER_PGSTAC=true." }, + { + "name": "USGS SF DEM \u2014 Custom Color Table (GDAL)", + "uuid": "fa3b9734-e223-40e1-a253-7bc9e63968a3", + "type": "tile", + "visibility": false, + "sourceType": "COG", + "url": "Data/DEMs/USGS_13_n38w123_20250826_SFHill.tif", + "tileformat": "tms", + "controlled": false, + "initialOpacity": 1, + "minZoom": 0, + "maxNativeZoom": 30, + "maxZoom": 30, + "demparser": "rgba", + "throughTileServer": true, + "cogResampling": "nearest", + "cogBands": [ + 1 + ], + "cogExpressionEditable": false, + "cogTransform": true, + "cogMin": 0, + "cogMax": 800, + "cogUnits": "m", + "cogColormapJson": "[[[-5,5],[70,130,180,255]],[[5,50],[90,170,120,255]],[[50,120],[150,200,110,255]],[[120,220],[220,220,120,255]],[[220,350],[210,170,90,255]],[[350,500],[180,120,70,255]],[[500,700],[150,90,60,255]],[[700,900],[240,240,240,255]]]", + "style": { + "brightness": 1, + "contrast": 1, + "saturation": 1, + "blend": "none" + }, + "time": { + "enabled": false, + "type": "requery", + "compositeTile": false, + "refreshIntervalEnabled": false + }, + "variables": { + "legendOrientation": "vertical" + }, + "description": "Demonstrates the Custom Color Table (GDAL) feature: the SF DEM colored by an interval color table (cogColormapJson) passed to TiTiler's colormap parameter, producing discrete elevation bands (blue sea level \u2192 green \u2192 tan \u2192 white peaks) with an auto-generated discrete legend and no rescale. Served as a direct COG via TiTiler \u2014 requires WITH_TITILER=true only (no STAC stack)." + }, { "name": "USGS SF DEM \u2014 Elevation", "uuid": "e57468ef-b6f9-473f-973a-59d9dacfe016", diff --git a/configure/package.json b/configure/package.json index e6435c75cb..7c7c4b7a46 100644 --- a/configure/package.json +++ b/configure/package.json @@ -1,6 +1,6 @@ { "name": "configure", - "version": "5.4.1-20260916", + "version": "5.4.2-20260922", "homepage": "./configure/build", "private": true, "dependencies": { diff --git a/configure/src/core/Maker.js b/configure/src/core/Maker.js index befdd1fd0d..037f0c45c2 100644 --- a/configure/src/core/Maker.js +++ b/configure/src/core/Maker.js @@ -617,6 +617,22 @@ const getComponent = ( const ewfVal = getIn(configuration, com.enableWhenField.field, com.enableWhenField.default || ""); disabled = disabled || ewfVal !== com.enableWhenField.value; } + if (com.disableWhen) { + // Disables the component when ALL conditions match. Each condition + // references a sibling field: {field, notEmpty: true} or {field, equals: } + const conditions = Array.isArray(com.disableWhen) + ? com.disableWhen + : [com.disableWhen]; + const allMet = conditions.every((cond) => { + const condVal = getIn(directConf, cond.field, null); + if (cond.notEmpty === true) + return condVal != null && String(condVal).trim() !== ""; + if (Object.prototype.hasOwnProperty.call(cond, "equals")) + return condVal === cond.equals; + return false; + }); + disabled = disabled || allMet; + } const isRequired = isFieldRequired(com, layer, configuration); const fieldValue = value != null ? value : configured(""); const hasError = isRequired && (fieldValue === "" || fieldValue == null); @@ -1629,7 +1645,15 @@ const getComponent = ( ) : ( - + {inner} )} diff --git a/configure/src/core/utils.js b/configure/src/core/utils.js index 12f7d8fbe1..e9fd6684f8 100644 --- a/configure/src/core/utils.js +++ b/configure/src/core/utils.js @@ -6,7 +6,8 @@ export const isNumeric = (value) => { }; export const isUrlAbsolute = (url) => { - const r = new RegExp("^(?:[a-z]+:)?//", "i"); + // RFC 3986 scheme (letters, digits, + - .) so s3://, gs://, etc. count + const r = new RegExp("^(?:[a-z][a-z0-9+.-]*:)?//", "i"); return r.test(url); }; diff --git a/configure/src/metaconfigs/tab-ui-config.json b/configure/src/metaconfigs/tab-ui-config.json index a0a46d8489..5fb46cbd69 100644 --- a/configure/src/metaconfigs/tab-ui-config.json +++ b/configure/src/metaconfigs/tab-ui-config.json @@ -443,6 +443,18 @@ } ] }, + { + "components": [ + { + "field": "look.showAttributionsOnMap", + "name": "Show Attributions on Map", + "description": "Always show a collapsible dropdown of the visible layers' attributions on the map (top-right, next to the home button). Attributions are always listed in the About modal regardless of this setting.", + "type": "checkbox", + "width": 4, + "defaultChecked": false + } + ] + }, { "name": "Default Tool", "components": [ diff --git a/docs/pages/Configure/Tabs/Look/Look_Tab.md b/docs/pages/Configure/Tabs/Look/Look_Tab.md index d2f635c4c8..7f3114b24a 100644 --- a/docs/pages/Configure/Tabs/Look/Look_Tab.md +++ b/docs/pages/Configure/Tabs/Look/Look_Tab.md @@ -95,3 +95,7 @@ A URL to a new logo if the default MMGIS is unwanted. The logo should be reasona #### Help URL The URL to take users to in they click the help icon in the bottom left. Help above should be checked on. + +#### Show Attributions on Map + +If checked, a collapsible dropdown listing the attributions of all currently visible layers is always shown on the map, to the left of the home/zoom controls. It is hidden entirely when no visible layer has an attribution. Attributions are always listed in the About modal regardless of this setting. diff --git a/docs/pages/Database/DrawTool/DrawTool.md b/docs/pages/Database/DrawTool/DrawTool.md index 49cc91ba13..492268eeed 100644 --- a/docs/pages/Database/DrawTool/DrawTool.md +++ b/docs/pages/Database/DrawTool/DrawTool.md @@ -39,6 +39,7 @@ The `user_files` table stores an entry for each file a user created via the Draw - `template`: A JSON defining rules that each feature's metadata must comply to. Templates can be set on files at creation or through an existing file's information modal. - `publicity_type`: If the file is public, how public? Read-only, list out other users who can draw and edit in it, all users can draw and edit in it. - `public_editors`: If the file is public and the publicity type is "list-editors", this is the list of usernames who can draw and edit in this file. +- `mission`: The name of the mission the file was created in. Files are only listed in the Draw Tool of their mission. `NULL` for files created before mission scoping existed; whether those legacy files are listed is controlled per mission by the Draw Tool's `showNullMissionFiles` variable (default `true`). Lead files (`is_master`) are shown in every mission unless the Draw Tool's `showMasterFiles` variable is `false`. Both variables are read server-side from the mission's saved configuration, so clients cannot override them. The file's mission is shown in its info modal; leads can move a file to another existing mission from there. Renaming a mission renames its files' `mission`; deleting a mission sets its files' `mission` back to `NULL`. ### file_histories diff --git a/package.json b/package.json index c774cb77df..43646905f5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mmgis", - "version": "5.4.1-20260916", + "version": "5.4.2-20260922", "description": "A web-based mapping and localization solution for science operation on planetary missions.", "homepage": "build", "repository": { diff --git a/plugins/core/backend/Config/routes/configs.js b/plugins/core/backend/Config/routes/configs.js index 12244e41a5..8507887313 100644 --- a/plugins/core/backend/Config/routes/configs.js +++ b/plugins/core/backend/Config/routes/configs.js @@ -14,6 +14,7 @@ const logger = require("../../../../../API/logger"); const Config = require("../models/config"); const config_template = require("../../../../../API/templates/config_template"); const userModel = require("../../Users/models/user"); +const Userfiles = require("../../Draw/models/userfiles").Userfiles; const User = userModel.User; const { UserDefaults } = require("../../Users/models/userdefaults"); const missionTemplates = require("../../Utils/missionTemplates"); @@ -1418,10 +1419,17 @@ if (fullAccess) return null; }); + // Draw files are scoped by mission name too. + const userfilesUpdate = Userfiles.update( + { mission: newName }, + { where: { mission: missionName }, transaction: t } + ); + return Promise.all([ ...configUpdates, permissionUpdate, defaultsUpdate, + userfilesUpdate, ]); }) .then(() => { @@ -1528,11 +1536,16 @@ if (fullAccess) return; } - Config.destroy({ - where: { - mission: missionName, - }, - }) + sequelize + .transaction((t) => + Promise.all([ + Config.destroy({ where: { mission: missionName }, transaction: t }), + Userfiles.update( + { mission: null }, + { where: { mission: missionName }, transaction: t } + ), + ]) + ) .then((mission) => { logger( "info", diff --git a/plugins/core/backend/Draw/models/userfiles.js b/plugins/core/backend/Draw/models/userfiles.js index c4077845a7..e07a51f2a5 100644 --- a/plugins/core/backend/Draw/models/userfiles.js +++ b/plugins/core/backend/Draw/models/userfiles.js @@ -94,6 +94,12 @@ const attributes = { unique: false, allowNull: true, }, + mission: { + type: Sequelize.STRING, + unique: false, + allowNull: true, + defaultValue: null, + }, }; const options = { @@ -201,6 +207,27 @@ const up = async () => { ); return null; }); + + // mission column + for (const table of ["user_files", "user_files_tests"]) { + await sequelize + .query( + `ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS mission varchar(255) NULL;` + ) + .then(() => { + return null; + }) + .catch((err) => { + logger( + "error", + `Failed to adding ${table}.mission column. DB tables may be out of sync!`, + "user_files", + null, + err + ); + return null; + }); + } }; // export User model for use in other files. diff --git a/plugins/core/backend/Draw/plugin.js b/plugins/core/backend/Draw/plugin.js index 8f38d04fd9..62087e3242 100644 --- a/plugins/core/backend/Draw/plugin.js +++ b/plugins/core/backend/Draw/plugin.js @@ -38,12 +38,12 @@ let setup = { //Once the server starts onceStarted: (s) => {}, //Once all tables sync - onceSynced: (s) => { + onceSynced: async (s) => { if (typeof file_histories.up === "function") { file_histories.up(); } if (typeof ufiles.up === "function") { - ufiles.up(); + await ufiles.up(); } routeFiles.makeMasterFiles([ "roi", diff --git a/plugins/core/backend/Draw/routes/files.js b/plugins/core/backend/Draw/routes/files.js index 5107fcc388..66c5479ad3 100644 --- a/plugins/core/backend/Draw/routes/files.js +++ b/plugins/core/backend/Draw/routes/files.js @@ -17,6 +17,7 @@ const published = require("../models/published"); const Published = published.Published; const PublishedTEST = published.PublishedTEST; const PublishedStore = require("../models/publishedstore"); +const Config = require("../../Config/models/config"); const draw = require("./draw"); const filesutils = require("./filesutils"); @@ -43,12 +44,61 @@ router.post("/", function (req, res, next) { res.send("test files"); }); +// Resolves to the mission's latest DrawTool `variables` ({} if the tool has +// none) or null if the mission does not exist. +function getDrawToolVars(mission) { + if (mission == null) return Promise.resolve(null); + return Config.findOne({ + where: { mission }, + order: [["version", "DESC"]], + }).then((row) => { + if (!row) return null; + const tools = (row.config && row.config.tools) || []; + const draw = tools.find( + (t) => t && typeof t.name === "string" && t.name.toLowerCase() === "draw" + ); + return (draw && draw.variables) || {}; + }); +} + /** * Gets all owned or public files + * { + * mission: (optional) only return files of this mission. Whether + * NULL-mission and master files are also returned is read server-side from + * the mission's DrawTool variables (showNullMissionFiles, showMasterFiles). + * } */ -router.post("/getfiles", function (req, res, next) { +router.post("/getfiles", async function (req, res, next) { let Table = req.body.test === "true" ? UserfilesTEST : Userfiles; + let showNullMissionFiles = true; + let showMasterFiles = true; + if (req.body.mission != null) { + let vars; + try { + vars = await getDrawToolVars(req.body.mission); + } catch (err) { + logger("error", "Failed to get files.", req.originalUrl, req, err); + res.send({ + status: "failure", + message: "Failed to get files.", + body: {}, + }); + return; + } + if (vars == null) { + res.send({ + status: "failure", + message: "Failed to get files. Unknown mission.", + body: {}, + }); + return; + } + showNullMissionFiles = vars.showNullMissionFiles !== false; + showMasterFiles = vars.showMasterFiles !== false; + } + const orWhere = [ { file_owner: req.user, @@ -63,11 +113,19 @@ router.post("/getfiles", function (req, res, next) { : "1", }, ]; + const andWhere = [{ hidden: "0" }, { [Sequelize.Op.or]: orWhere }]; + + if (req.body.mission != null) { + const missionOr = [{ mission: req.body.mission }]; + if (showNullMissionFiles) missionOr.push({ mission: null }); + if (showMasterFiles) missionOr.push({ is_master: true }); + andWhere.push({ [Sequelize.Op.or]: missionOr }); + } + if (!showMasterFiles) andWhere.push({ is_master: { [Sequelize.Op.ne]: true } }); + Table.findAll({ where: { - //file_owner is req.user or public is '0' - hidden: "0", - [Sequelize.Op.or]: orWhere, + [Sequelize.Op.and]: andWhere, }, }) .then((files) => { @@ -114,11 +172,29 @@ router.post("/getfile", getfile); * file_description: (optional) * intent: (optional) * geojson: (optional) -- geojson to initialize file from + * mission: (optional) -- mission this file belongs to (must exist) * } */ -router.post("/make", function (req, res, next) { +router.post("/make", async function (req, res, next) { let Table = req.body.test === "true" ? UserfilesTEST : Userfiles; + if (req.body.mission != null) { + let vars = null; + try { + vars = await getDrawToolVars(req.body.mission); + } catch (err) { + logger("error", "Failed to make a new file.", req.originalUrl, req, err); + } + if (vars == null) { + res.send({ + status: "failure", + message: "Failed to make a new file. Unknown mission.", + body: {}, + }); + return; + } + } + //group is a reserved keyword if (req.user === "group") { logger( @@ -146,6 +222,7 @@ router.post("/make", function (req, res, next) { publicity_type: "read_only", hidden: "0", template: req.body.template ? JSON.parse(req.body.template) : null, + mission: req.body.mission || null, }; // Insert new userfile into the user_files table @@ -419,10 +496,12 @@ router.post("/restore", function (req, res, next) { * template: (optional) * publicity_type: (optional) * public_editors: (optional) + * mission: (optional, leads only) move the file to an existing mission * } */ -router.post("/change", function (req, res, next) { +router.post("/change", async function (req, res, next) { let Table = req.body.test === "true" ? UserfilesTEST : Userfiles; + const isLead = req.groups && req.groups["mmgis-group"] === true; //Form update object let toUpdateTo = {}; @@ -464,6 +543,23 @@ router.post("/change", function (req, res, next) { toUpdateTo.public_editors = public_editors; } catch (err) {} } + if (isLead && req.body.mission != null && req.body.mission !== "") { + let vars = null; + try { + vars = await getDrawToolVars(req.body.mission); + } catch (err) { + logger("error", "Failed to edit file.", req.originalUrl, req, err); + } + if (vars == null) { + res.send({ + status: "failure", + message: "Failed to edit file. Unknown mission.", + body: {}, + }); + return; + } + toUpdateTo.mission = req.body.mission; + } let updateObj = { where: { @@ -474,7 +570,7 @@ router.post("/change", function (req, res, next) { }; // Alow leads to edit file info - if (req.groups && req.groups["mmgis-group"] === true) + if (isLead) updateObj = { where: { id: req.body.id, diff --git a/plugins/core/backend/Webhooks/routes/buildEntriesResponse.js b/plugins/core/backend/Webhooks/routes/buildEntriesResponse.js new file mode 100644 index 0000000000..c7691e2a99 --- /dev/null +++ b/plugins/core/backend/Webhooks/routes/buildEntriesResponse.js @@ -0,0 +1,13 @@ +function buildEntriesResponse(sets) { + const entries = (sets || []).map((set) => ({ + config: set.config, + updated: set.updatedAt, + })); + + return { + status: "success", + body: { entries }, + }; +} + +module.exports = buildEntriesResponse; diff --git a/plugins/core/backend/Webhooks/routes/webhookutils.js b/plugins/core/backend/Webhooks/routes/webhookutils.js index c73a144030..0bc6b3eae2 100644 --- a/plugins/core/backend/Webhooks/routes/webhookutils.js +++ b/plugins/core/backend/Webhooks/routes/webhookutils.js @@ -4,6 +4,7 @@ **********************************************************/ const logger = require("../../../../../API/logger"); const Webhooks = require("../models/webhooks"); +const buildEntriesResponse = require("./buildEntriesResponse"); function entries(req, res, next) { logger("success", "Called /webhooks/entries API", req.originalUrl, req); @@ -11,17 +12,7 @@ function entries(req, res, next) { order: [["updatedAt", "DESC"]], }) .then((sets) => { - if (sets && sets.length > 0) { - let entries = []; - for (let i = 0; i < sets.length; i++) { - entries.push({ config: sets[i].config, updated: sets[i].updatedAt }); - } - - res.send({ - status: "success", - body: { entries: entries }, - }); - } + res.send(buildEntriesResponse(sets)); }) .catch((err) => { logger("error", "Failure finding webhooks.", req.originalUrl, req, err); diff --git a/plugins/core/layertypes/Image/legend.js b/plugins/core/layertypes/Image/legend.js index 63d98ad81e..6f7902f846 100644 --- a/plugins/core/layertypes/Image/legend.js +++ b/plugins/core/layertypes/Image/legend.js @@ -8,7 +8,10 @@ import ToolController_ from '@basics/ToolController_/ToolController_' // Build the layer's legend from its live render parameters. The scale itself is // drawn by the LayersTool, which owns the colormap/rescale UI it comes from. function derive(layerObj) { - if (layerObj.cogTransform !== true) return false + // A custom GDAL color table (cogColormapJson) colors raw pixel values + // without the COG transform, so it yields a legend on its own. + if (layerObj.cogTransform !== true && layerObj.cogColormapJson == null) + return false ToolController_.getTool('LayersTool')?.populateCogScale(layerObj.name) return true diff --git a/plugins/core/layertypes/Tile/legend.js b/plugins/core/layertypes/Tile/legend.js index 3b3f219798..b7fce8b85b 100644 --- a/plugins/core/layertypes/Tile/legend.js +++ b/plugins/core/layertypes/Tile/legend.js @@ -8,7 +8,10 @@ import ToolController_ from '@basics/ToolController_/ToolController_' // Build the layer's legend from its live render parameters. The scale itself is // drawn by the LayersTool, which owns the colormap/rescale UI it comes from. function derive(layerObj) { - if (layerObj.cogTransform !== true) return false + // A custom GDAL color table (cogColormapJson) colors raw pixel values + // without the COG transform, so it yields a legend on its own. + if (layerObj.cogTransform !== true && layerObj.cogColormapJson == null) + return false ToolController_.getTool('LayersTool')?.populateCogScale(layerObj.name) return true diff --git a/plugins/core/layertypes/Tile/map.js b/plugins/core/layertypes/Tile/map.js index 18dddd0016..7b861b0dde 100644 --- a/plugins/core/layertypes/Tile/map.js +++ b/plugins/core/layertypes/Tile/map.js @@ -134,6 +134,7 @@ async function make(layerObj, ctx = {}) { cogMax: layerObj.cogMax, currentCogMax: layerObj.currentCogMax, cogColormap: layerObj.cogColormap, + cogColormapJson: layerObj.cogColormapJson, cogExpression: layerObj.cogExpression, currentCogExpression: layerObj.currentCogExpression, variables: layerObj.variables || {}, diff --git a/plugins/core/layertypes/Tile/plugin.json b/plugins/core/layertypes/Tile/plugin.json index 7644b5c37a..81b9e9cb88 100644 --- a/plugins/core/layertypes/Tile/plugin.json +++ b/plugins/core/layertypes/Tile/plugin.json @@ -365,7 +365,24 @@ "description": "", "type": "colordropdown", "width": 3, - "options": "{{COLORMAP_NAMES}}" + "options": "{{COLORMAP_NAMES}}", + "disableWhen": [ + { "field": "cogColormapJson", "notEmpty": true }, + { "field": "cogTransform", "equals": true } + ], + "disabledMessage": "A Custom Color Table (GDAL) is set below and supersedes this colormap. Clear the custom color table to use a named colormap." + } + ] + }, + { + "name": "Custom Color Table (GDAL)", + "components": [ + { + "field": "cogColormapJson", + "name": "Color Table", + "description": "A custom GDAL color table sent to TiTiler's 'colormap' parameter, mapping raw pixel values to colors. Provide JSON as either a discrete value-to-RGBA map (e.g. {\"0\":[0,0,0,255],\"255\":[255,255,255,255]}) or a list of interval-to-RGBA entries (e.g. [[[0,100],[0,0,0,255]],[[100,200],[255,255,255,255]]]). When set, this supersedes the named Colormap of the Single Data Band COG Transformation, no rescaling is applied, and a discrete legend is generated automatically.", + "type": "textnotrim", + "width": 12 } ] } @@ -742,7 +759,7 @@ }, { "subname": "Attribution", - "subdescription": "Attribution information for the dataset. If configured, the attribution will be displayed horizontally below the scalebar when the layer is visible.", + "subdescription": "Attribution information for the dataset. If configured, the attribution is listed in the About modal while the layer is visible and, if 'Show Attributions on Map' is enabled in the Look tab, in the on-map attributions dropdown.", "components": [ { "field": "attribution", diff --git a/plugins/core/tools/Draw/DrawTool.css b/plugins/core/tools/Draw/DrawTool.css index 1e0a87f689..4aefa8c1d4 100644 --- a/plugins/core/tools/Draw/DrawTool.css +++ b/plugins/core/tools/Draw/DrawTool.css @@ -1126,7 +1126,7 @@ } .drawToolFileEditOn { - min-width: 400px; + min-width: 420px; color: var(--color-f); background: var(--color-a); box-shadow: 0px 2px 6px 0px rgba(0, 0, 0, 0.5); @@ -1221,7 +1221,7 @@ border: none; border-top: 1px solid black; color: var(--color-f); - min-width: 452px; + min-width: 472px; max-width: 750px; min-height: 100px; max-height: 400px; @@ -1246,6 +1246,15 @@ .drawToolFileEditOnDates > div > div:last-child { padding: 0px 4px; } +.drawToolFileEditOnMission { + padding: 4px 10px; +} +.drawToolFileEditOnMission > div { + align-items: center; +} +#drawToolFileEditOnMissionDropdown { + min-width: 160px; +} .drawToolFileEditOnC { display: flex; justify-content: space-between; diff --git a/plugins/core/tools/Draw/DrawTool.js b/plugins/core/tools/Draw/DrawTool.js index 810b9d62c1..dcb3648a1f 100644 --- a/plugins/core/tools/Draw/DrawTool.js +++ b/plugins/core/tools/Draw/DrawTool.js @@ -1256,7 +1256,9 @@ var DrawTool = { } calls.api( 'files_getfiles', - {}, + { + mission: L_.mission, + }, function (data) { if (data && data.body) { //sort files by intent and then alphabetically by name within intent @@ -1302,6 +1304,7 @@ var DrawTool = { }, makeFile: function (body, callback) { const filename = body.file_name + body.mission = L_.mission calls.api( 'files_make', body, diff --git a/plugins/core/tools/Draw/DrawTool_Files.js b/plugins/core/tools/Draw/DrawTool_Files.js index e1182569e5..0b524221c7 100644 --- a/plugins/core/tools/Draw/DrawTool_Files.js +++ b/plugins/core/tools/Draw/DrawTool_Files.js @@ -7,6 +7,9 @@ import Globe_ from '@basics/Globe_/Globe_' import Map_ from '@basics/Map_/Map_' import CursorInfo from '@basics/UserInterface_/components/CursorInfo/CursorInfo' import Toast from '@design/components/Toast/Toast' +import Select from '@design/components/Select/Select' +import React from 'react' +import { createRoot } from 'react-dom/client' import Modal from '@basics/UserInterface_/components/Modal/Modal' import DrawTool_Templater from './DrawTool_Templater' @@ -85,6 +88,8 @@ var Files = { Files.recalculateFolderCounts() + $('#drawToolMaster').toggle(DrawTool.files.some((f) => f.is_master)) + //Master Header $('.drawToolMasterHeaderLeftLeft').off('click') $('.drawToolMasterHeaderLeftLeft').on('click', function () { @@ -957,6 +962,12 @@ var Files = { ) .join('\n') + const missionMarkup = isLead && !file.is_master + ? `
` + : `
${safeHTML(file.mission || 'NONE')}
` + let selectedMission = file.mission || '' + let missionRoot = null + // prettier-ignore const modalContentEditable = [ "
", @@ -995,6 +1006,12 @@ var Files = { `
${file.template?.name || 'NONE'}
`, "
", "", + "
", + "
", + "
Mission:
", + missionMarkup, + "
", + "
", "
", "", "
", @@ -1061,6 +1078,12 @@ var Files = { `
${template?.name || 'NONE'}
`, "", "", + "
", + "
", + "
Mission:
", + missionMarkup, + "
", + "
", "
", "", "
", @@ -1080,7 +1103,10 @@ var Files = { "", "
", "
", - "
Cancel
", + "
", + "
Cancel
", + isLead && !file.is_master ? "
Save
" : "", + "
", "
", "" ].join('\n') @@ -1092,6 +1118,38 @@ var Files = { ? modalContentEditable : modalContent, function () { + if (isLead) { + calls.api( + 'missions', + {}, + function (s) { + const container = document.getElementById( + 'drawToolFileEditOnMissionDropdown' + ) + if (!container) return + const options = (s.missions || []).map( + (m) => ({ value: m, label: m }) + ) + if (!file.mission) + options.unshift({ value: '', label: 'NONE' }) + const render = () => + missionRoot.render( +