diff --git a/API/Backend/Draw/models/filehistories.js b/API/Backend/Draw/models/filehistories.js index 4640e30b..034d20b2 100644 --- a/API/Backend/Draw/models/filehistories.js +++ b/API/Backend/Draw/models/filehistories.js @@ -43,6 +43,11 @@ const attributes = { type: Sequelize.DataTypes.ARRAY(Sequelize.DataTypes.INTEGER), allowNull: true, }, + author: { + type: Sequelize.STRING, + unique: false, + allowNull: true, + }, }; const options = { @@ -57,5 +62,27 @@ var FilehistoriesTEST = sequelize.define( options ); +// Adds to the table, never removes +const up = async () => { + // author column + await sequelize + .query( + `ALTER TABLE file_histories ADD COLUMN IF NOT EXISTS author varchar(255) NULL;` + ) + .then(() => { + return null; + }) + .catch((err) => { + logger( + "error", + `Failed to adding file_histories.author column. DB tables may be out of sync!`, + "file_histories", + null, + err + ); + return null; + }); +}; + // export Filehistories model for use in other files. -module.exports = { Filehistories, FilehistoriesTEST }; +module.exports = { Filehistories, FilehistoriesTEST, up }; diff --git a/API/Backend/Draw/models/userfiles.js b/API/Backend/Draw/models/userfiles.js index e6dc8c8d..f701f743 100644 --- a/API/Backend/Draw/models/userfiles.js +++ b/API/Backend/Draw/models/userfiles.js @@ -84,6 +84,16 @@ const attributes = { allowNull: true, defaultValue: null, }, + publicity_type: { + type: Sequelize.STRING, + unique: false, + allowNull: true, + }, + public_editors: { + type: Sequelize.ARRAY(Sequelize.TEXT), + unique: false, + allowNull: true, + }, }; const options = { @@ -153,6 +163,44 @@ const up = async () => { ); return null; }); + + // publicity_type column + await sequelize + .query( + `ALTER TABLE user_files ADD COLUMN IF NOT EXISTS publicity_type varchar(255) NULL;` + ) + .then(() => { + return null; + }) + .catch((err) => { + logger( + "error", + `Failed to adding user_files.publicity_type column. DB tables may be out of sync!`, + "user_files", + null, + err + ); + return null; + }); + + // public_editors column + await sequelize + .query( + `ALTER TABLE user_files ADD COLUMN IF NOT EXISTS public_editors text[] NULL;` + ) + .then(() => { + return null; + }) + .catch((err) => { + logger( + "error", + `Failed to adding user_files.public_editors 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/API/Backend/Draw/routes/draw.js b/API/Backend/Draw/routes/draw.js index bab38b8b..703d0bba 100644 --- a/API/Backend/Draw/routes/draw.js +++ b/API/Backend/Draw/routes/draw.js @@ -49,6 +49,7 @@ const pushToHistory = ( time, undoToTime, action_index, + user, successCallback, failureCallback ) => { @@ -85,6 +86,7 @@ const pushToHistory = ( time: time, action_index: action_index, history: h, + author: user, }; // Insert new entry into the history table Table.create(newHistoryEntry) @@ -252,6 +254,7 @@ const clipOver = function ( time, null, 5, + req.user, () => { if (typeof successCallback === "function") successCallback(); }, @@ -392,6 +395,7 @@ const clipUnder = function ( time, null, 7, + req.user, () => { if (typeof successCallback === "function") successCallback(); }, @@ -475,13 +479,28 @@ const add = function ( Files.findOne({ where: { id: req.body.file_id, - [Sequelize.Op.or]: { - file_owner: req.user, - [Sequelize.Op.and]: { - file_owner: "group", - file_owner_group: { [Sequelize.Op.overlap]: groups }, + [Sequelize.Op.or]: [ + { file_owner: req.user }, + { + [Sequelize.Op.and]: { + file_owner: "group", + file_owner_group: { [Sequelize.Op.overlap]: groups }, + }, }, - }, + { + [Sequelize.Op.and]: { + public: "1", + publicity_type: "list_edit", + public_editors: { [Sequelize.Op.contains]: [req.user] }, + }, + }, + { + [Sequelize.Op.and]: { + public: "1", + publicity_type: "all_edit", + }, + }, + ], }, }).then((file) => { if (!file) { @@ -581,6 +600,7 @@ const add = function ( time, null, 0, + req.user, () => { if (typeof successCallback === "function") successCallback(created.id, created.intent); @@ -665,13 +685,28 @@ const edit = function (req, res, successCallback, failureCallback) { Files.findOne({ where: { id: req.body.file_id, - [Sequelize.Op.or]: { - file_owner: req.user, - [Sequelize.Op.and]: { - file_owner: "group", - file_owner_group: { [Sequelize.Op.overlap]: groups }, + [Sequelize.Op.or]: [ + { file_owner: req.user }, + { + [Sequelize.Op.and]: { + file_owner: "group", + file_owner_group: { [Sequelize.Op.overlap]: groups }, + }, }, - }, + { + [Sequelize.Op.and]: { + public: "1", + publicity_type: "list_edit", + public_editors: { [Sequelize.Op.contains]: [req.user] }, + }, + }, + { + [Sequelize.Op.and]: { + public: "1", + publicity_type: "all_edit", + }, + }, + ], }, }) .then((file) => { @@ -749,6 +784,7 @@ const edit = function (req, res, successCallback, failureCallback) { time, null, 1, + req.user, () => { successCallback(createdId, createdUUID, createdIntent); }, @@ -822,13 +858,28 @@ router.post("/remove", function (req, res, next) { Files.findOne({ where: { id: req.body.file_id, - [Sequelize.Op.or]: { - file_owner: req.user, - [Sequelize.Op.and]: { - file_owner: "group", - file_owner_group: { [Sequelize.Op.overlap]: groups }, + [Sequelize.Op.or]: [ + { file_owner: req.user }, + { + [Sequelize.Op.and]: { + file_owner: "group", + file_owner_group: { [Sequelize.Op.overlap]: groups }, + }, }, - }, + { + [Sequelize.Op.and]: { + public: "1", + publicity_type: "list_edit", + public_editors: { [Sequelize.Op.contains]: [req.user] }, + }, + }, + { + [Sequelize.Op.and]: { + public: "1", + publicity_type: "all_edit", + }, + }, + ], }, }).then((file) => { if (!file) { @@ -861,6 +912,7 @@ router.post("/remove", function (req, res, next) { time, null, 2, + req.user, () => { logger("info", "Feature removed.", req.originalUrl, req); res.send({ @@ -927,13 +979,28 @@ router.post("/undo", function (req, res, next) { Files.findOne({ where: { id: req.body.file_id, - [Sequelize.Op.or]: { - file_owner: req.user, - [Sequelize.Op.and]: { - file_owner: "group", - file_owner_group: { [Sequelize.Op.overlap]: groups }, + [Sequelize.Op.or]: [ + { file_owner: req.user }, + { + [Sequelize.Op.and]: { + file_owner: "group", + file_owner_group: { [Sequelize.Op.overlap]: groups }, + }, }, - }, + { + [Sequelize.Op.and]: { + public: "1", + publicity_type: "list_edit", + public_editors: { [Sequelize.Op.contains]: [req.user] }, + }, + }, + { + [Sequelize.Op.and]: { + public: "1", + publicity_type: "all_edit", + }, + }, + ], }, }).then((file) => { if (!file) { @@ -992,6 +1059,7 @@ router.post("/undo", function (req, res, next) { time, req.body.undo_time, 3, + req.user, () => { logger("info", "Undo successful.", req.originalUrl, req); res.send({ @@ -1052,13 +1120,28 @@ router.post("/merge", function (req, res, next) { Files.findOne({ where: { id: req.body.file_id, - [Sequelize.Op.or]: { - file_owner: req.user, - [Sequelize.Op.and]: { - file_owner: "group", - file_owner_group: { [Sequelize.Op.overlap]: groups }, + [Sequelize.Op.or]: [ + { file_owner: req.user }, + { + [Sequelize.Op.and]: { + file_owner: "group", + file_owner_group: { [Sequelize.Op.overlap]: groups }, + }, }, - }, + { + [Sequelize.Op.and]: { + public: "1", + publicity_type: "list_edit", + public_editors: { [Sequelize.Op.contains]: [req.user] }, + }, + }, + { + [Sequelize.Op.and]: { + public: "1", + publicity_type: "all_edit", + }, + }, + ], }, }).then((file) => { if (!file) { @@ -1131,6 +1214,7 @@ router.post("/merge", function (req, res, next) { time, null, 6, + req.user, () => { logger( "info", @@ -1216,13 +1300,28 @@ router.post("/split", function (req, res, next) { Files.findOne({ where: { id: req.body.file_id, - [Sequelize.Op.or]: { - file_owner: req.user, - [Sequelize.Op.and]: { - file_owner: "group", - file_owner_group: { [Sequelize.Op.overlap]: groups }, + [Sequelize.Op.or]: [ + { file_owner: req.user }, + { + [Sequelize.Op.and]: { + file_owner: "group", + file_owner_group: { [Sequelize.Op.overlap]: groups }, + }, }, - }, + { + [Sequelize.Op.and]: { + public: "1", + publicity_type: "list_edit", + public_editors: { [Sequelize.Op.contains]: [req.user] }, + }, + }, + { + [Sequelize.Op.and]: { + public: "1", + publicity_type: "all_edit", + }, + }, + ], }, }) .then((file) => { @@ -1290,6 +1389,7 @@ router.post("/split", function (req, res, next) { time, null, 8, + req.user, () => { res.send({ status: "success", diff --git a/API/Backend/Draw/routes/files.js b/API/Backend/Draw/routes/files.js index 0ec36f34..f0d4f4cf 100644 --- a/API/Backend/Draw/routes/files.js +++ b/API/Backend/Draw/routes/files.js @@ -48,14 +48,25 @@ router.post("/", function (req, res, next) { router.post("/getfiles", function (req, res, next) { let Table = req.body.test === "true" ? UserfilesTEST : Userfiles; + const orWhere = [ + { + file_owner: req.user, + }, + { public: "1" }, + { + public: + req.leadGroupName != null && + req.groups != null && + req.groups[req.leadGroupName] === true + ? "0" + : "1", + }, + ]; Table.findAll({ where: { //file_owner is req.user or public is '0' hidden: "0", - [Sequelize.Op.or]: { - file_owner: req.user, - public: "1", - }, + [Sequelize.Op.or]: orWhere, }, }) .then((files) => { @@ -131,6 +142,7 @@ router.post("/make", function (req, res, next) { file_description: req.body.file_description, intent: req.body.intent, public: "1", + publicity_type: "read_only", hidden: "0", template: req.body.template ? JSON.parse(req.body.template) : null, }; @@ -403,6 +415,9 @@ router.post("/restore", function (req, res, next) { * file_name: (optional) * file_description: (optional) * public: <0|1> (optional) + * template: (optional) + * publicity_type: (optional) + * public_editors: (optional) * } */ router.post("/change", function (req, res, next) { @@ -430,6 +445,24 @@ router.post("/change", function (req, res, next) { toUpdateTo.template = JSON.parse(req.body.template); } catch (err) {} } + if ( + req.body.hasOwnProperty("publicity_type") && + [null, "read_only", "list_edit", "all_edit"].includes( + req.body.publicity_type + ) + ) { + toUpdateTo.publicity_type = req.body.publicity_type; + } + if (req.body.hasOwnProperty("public_editors")) { + try { + let public_editors = null; + if (typeof req.body.public_editors === "string") + public_editors = req.body.public_editors + .split(",") + .map((e) => e.trim()); + toUpdateTo.public_editors = public_editors; + } catch (err) {} + } let updateObj = { where: { diff --git a/API/Backend/Draw/setup.js b/API/Backend/Draw/setup.js index 23901cf6..d74efb19 100644 --- a/API/Backend/Draw/setup.js +++ b/API/Backend/Draw/setup.js @@ -2,6 +2,7 @@ const routeFiles = require("./routes/files"); const routerFiles = routeFiles.router; const routerDraw = require("./routes/draw").router; const ufiles = require("./models/userfiles"); +const file_histories = require("./models/filehistories"); let setup = { //Once the app initializes @@ -28,6 +29,9 @@ let setup = { onceStarted: (s) => {}, //Once all tables sync onceSynced: (s) => { + if (typeof file_histories.up === "function") { + file_histories.up(); + } if (typeof ufiles.up === "function") { ufiles.up(); } diff --git a/src/essence/Basics/Formulae_/Formulae_.js b/src/essence/Basics/Formulae_/Formulae_.js index 886c1743..bcf81a83 100644 --- a/src/essence/Basics/Formulae_/Formulae_.js +++ b/src/essence/Basics/Formulae_/Formulae_.js @@ -1783,7 +1783,7 @@ var Formulae_ = { image.style.color = stringToTest return image.style.color !== 'rgb(255, 255, 255)' }, - timestampToDate(timestamp) { + timestampToDate(timestamp, small) { var a = new Date(timestamp * 1000) var months = [ 'Jan', @@ -1810,6 +1810,19 @@ var Formulae_ = { var sec = a.getUTCSeconds() < 10 ? '0' + a.getUTCSeconds() : a.getUTCSeconds() + if (small) { + return ( + month + + '/' + + date + + '/' + + (year + '').slice(-2) + + ' ' + + hour + + ':' + + min + ) + } return ( monthName + ' ' + diff --git a/src/essence/Tools/Draw/DrawTool.css b/src/essence/Tools/Draw/DrawTool.css index b358e4e5..e7e62e23 100644 --- a/src/essence/Tools/Draw/DrawTool.css +++ b/src/essence/Tools/Draw/DrawTool.css @@ -1364,6 +1364,32 @@ font-size: 12px; padding: 0px 5px 0px 9px; } +.drawToolFileEditListEditors { + height: 31px; + display: flex; + justify-content: space-between; + border-top: 1px solid black; + border-bottom: 1px solid black; + box-sizing: initial; +} +.drawToolFileEditListEditors > div { + line-height: 31px; + padding: 0px 8px; + font-size: 12px; + color: var(--color-a4); +} +.drawToolFileEditListEditors > input { + width: 180px; + height: 31px; + background: var(--color-a1-5); + border: none; + padding: 0px 8px; + font-size: 14px; + color: var(--color-mw2); + flex: 1 1; + border-left: 1px solid black; + transition: background 0.2s ease-out, border 0.2s ease-out; +} .drawToolFileEditOnDescription { display: flex; justify-content: space-between; @@ -2001,7 +2027,6 @@ transition: height 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94); } #drawToolHistorySequenceList { - margin: 0px 2px 0px 5px; flex: 1; overflow-y: auto; } @@ -2015,8 +2040,8 @@ #drawToolHistorySequenceList > ul > li { color: var(--color-f); background: rgba(0, 0, 0, 0); - border-bottom: 1px solid rgba(0, 0, 17, 0.25); - padding: 2px 5px; + border-top: 1px solid var(--color-a1-5); + padding: 3px 8px; font-size: 13px; cursor: pointer; transition: background 0.2s cubic-bezier(0.39, 0.575, 0.565, 1); diff --git a/src/essence/Tools/Draw/DrawTool_Files.js b/src/essence/Tools/Draw/DrawTool_Files.js index f4175742..64a4ded1 100644 --- a/src/essence/Tools/Draw/DrawTool_Files.js +++ b/src/essence/Tools/Draw/DrawTool_Files.js @@ -352,13 +352,21 @@ var Files = { ) ownedByUser = true + const isListEdit = + file.public == '1' && + file.publicity_type == 'list_edit' && + typeof file.public_editors?.includes === 'function' && + (file.public_editors.includes(mmgisglobal.user) || ownedByUser) + const isAllEdit = + file.public == '1' && file.publicity_type == 'all_edit' + // prettier-ignore var markup = [ "
", "
", "
", "
", - "", + "", `
${file.file_name}
`, "
", "
", @@ -939,11 +947,17 @@ var Files = { "
", `
by ${file.file_owner}${ownedByUser ? ' (you)' : ''}
`, "", "
", "
", + `
`, + "
File Editors:
", + ``, + "
", "
", "
", "
Created:
", @@ -1269,7 +1283,17 @@ var Files = { $('#drawToolFileEditOnTagsNew').val('') existingTagFol[type].push(newTag) } - + $('#drawToolFileEditOnPublicityDropdown').on( + 'change', + function () { + $('.drawToolFileEditListEditors').css({ + display: + $(this).val() === 'public list_edit' + ? 'flex' + : 'none', + }) + } + ) $('#drawToolFileEditOnTagsNewAdd').on('click', function () { tagFolderAdd('tags') }) @@ -1344,10 +1368,25 @@ var Files = { .find( '#drawToolFileEditOnPublicityDropdown' ) - .val() == 'public' + .val() + .indexOf('public') != -1 ? 1 : 0, template: JSON.stringify(template), + publicity_type: elm + .find('#drawToolFileEditOnPublicityDropdown') + .val() + .includes('public') + ? elm + .find( + '#drawToolFileEditOnPublicityDropdown' + ) + .val() + .replace('public ', '') + : null, + public_editors: elm + .find('#drawToolFileEditListEditors') + .val(), } DrawTool.changeFile( @@ -1580,13 +1619,30 @@ var Files = { // Only select files you own const fileId = $(this).attr('file_id') var fileFromId = DrawTool.getFileObjectWithId(fileId) + if ( - mmgisglobal.user !== $(this).attr('file_owner') && + mmgisglobal.user !== fileFromId.file_owner && fileFromId && F_.diff(fileFromId.file_owner_group, DrawTool.userGroups) .length == 0 - ) - return + ) { + // Now check public list_edit + if ( + !( + (fileFromId.public == '1' && + fileFromId.publicity_type == 'all_edit') || + (fileFromId.public == '1' && + fileFromId.publicity_type == 'list_edit' && + fileFromId.public_editors != null && + typeof fileFromId.public_editors.includes === + 'function' && + fileFromId.public_editors.includes( + mmgisglobal.user + )) + ) + ) + return + } const wasOn = $(this).parent().parent().hasClass('checked') diff --git a/src/essence/Tools/Draw/DrawTool_History.js b/src/essence/Tools/Draw/DrawTool_History.js index 0e0f7b01..987a4b06 100644 --- a/src/essence/Tools/Draw/DrawTool_History.js +++ b/src/essence/Tools/Draw/DrawTool_History.js @@ -90,7 +90,10 @@ var History = { var markup = [ "
", "" + h.message + "", - "" + F_.timestampToDate(h.time / 1000) + "", + "", + `${h.author ? `${h.author} - ` : ''}`, + `${F_.timestampToDate(h.time / 1000, true)}`, + "", "
" ].join('\n'); diff --git a/src/essence/Tools/Draw/DrawTool_Shapes.js b/src/essence/Tools/Draw/DrawTool_Shapes.js index 2ade5709..6566d1c0 100644 --- a/src/essence/Tools/Draw/DrawTool_Shapes.js +++ b/src/essence/Tools/Draw/DrawTool_Shapes.js @@ -660,6 +660,23 @@ var Shapes = { }) for (var i = 0; i < DrawTool.files.length; i++) { + const file = DrawTool.files[i] + let ownedByUser = false + if ( + mmgisglobal.user == file.file_owner || + (file.file_owner_group && + F_.diff(file.file_owner_group, DrawTool.userGroups) + .length > 0) + ) + ownedByUser = true + const isListEdit = + file.public == '1' && + file.publicity_type == 'list_edit' && + typeof file.public_editors?.includes === 'function' && + (file.public_editors.includes(mmgisglobal.user) || + ownedByUser) + const isAllEdit = + file.public == '1' && file.publicity_type == 'all_edit' //Lead Files if ( DrawTool.userGroups.indexOf('mmgis-group') != -1 && @@ -684,7 +701,9 @@ var Shapes = { .attr('value', DrawTool.files[i].id) .text(DrawTool.files[i].file_name + ' [Lead]') } else if ( - mmgisglobal.user == DrawTool.files[i].file_owner && + (mmgisglobal.user == DrawTool.files[i].file_owner || + isListEdit || + isAllEdit) && filenames.indexOf(DrawTool.files[i].file_name) == -1 && intent == DrawTool.files[i].intent && DrawTool.files[i].hidden == '0' diff --git a/src/essence/essence.js b/src/essence/essence.js index 7d3b35a9..8e368618 100644 --- a/src/essence/essence.js +++ b/src/essence/essence.js @@ -104,7 +104,7 @@ $(document).keyup(function (e) { // On tab, add tab styles if (e.which == '9' && !tabFocusAdded) { document.styleSheets[0].insertRule( - '.toolButton:focus,#barBottom > i:focus,#topBarTitleName:focus,.mainInfo > div:focus,#mainDescription:focus,#SearchType:focus,#auto_search:focus,#loginoutButton:focus,#mapSplitInnerLeft:focus,#mapSplitInnerRight:focus,#globeSplitInnerLeft:focus,#globeSplitInnerRight:focus {border: 2px solid var(--color-mmgis) !important;}', + '.toolButton:focus,#barBottom > i:focus,#topBarTitleName:focus,.mainInfo > div:focus,#mainDescription:focus,#SearchType:focus,#auto_search:focus,#loginoutButton:focus,#mapSplitInnerLeft:focus,#mapSplitInnerRight:focus,#globeSplitInnerLeft:focus,#globeSplitInnerRight:focus {box-shadow: inset 0px 0px 0px 2px var(--color-mmgis) !important;}', 1 ) tabFocusAdded = true