diff --git a/backend/actions/CaseReport/buildDocumentDataForAISummary.js b/backend/actions/CaseReport/buildDocumentDataForAISummary.js new file mode 100644 index 00000000..8222f2aa --- /dev/null +++ b/backend/actions/CaseReport/buildDocumentDataForAISummary.js @@ -0,0 +1,46 @@ +'use strict'; + +/** + * Load full documents from the app DB for each case-report document entry (for AI prompts). + * @param {import('mongoose').Connection} db + * @param {Array<{ documentId?: unknown, document?: unknown, documentModel: string, notes?: string }>} docEntries + */ +module.exports = async function buildDocumentDataForAISummary(db, docEntries) { + const documentData = []; + if (!Array.isArray(docEntries)) { + return documentData; + } + for (const docEntry of docEntries) { + if (!docEntry || !docEntry.documentModel) { + continue; + } + const rawId = docEntry.documentId != null ? docEntry.documentId : docEntry.document; + if (rawId == null || rawId === '') { + continue; + } + let documentId = rawId; + if (documentId != null && typeof documentId === 'object' && typeof documentId.toString === 'function') { + documentId = documentId.toString(); + } else { + documentId = String(documentId); + } + try { + const Model = db.models[docEntry.documentModel]; + if (!Model) { + continue; + } + const doc = await Model.findById(documentId).setOptions({ sanitizeFilter: true }).lean(); + if (doc) { + documentData.push({ + model: docEntry.documentModel, + documentId: documentId.toString(), + data: doc, + notes: docEntry.notes || '' + }); + } + } catch (err) { + console.error(`Error fetching document ${documentId} from model ${docEntry.documentModel}:`, err); + } + } + return documentData; +}; diff --git a/backend/actions/CaseReport/createCaseReport.js b/backend/actions/CaseReport/createCaseReport.js new file mode 100644 index 00000000..895ffea8 --- /dev/null +++ b/backend/actions/CaseReport/createCaseReport.js @@ -0,0 +1,93 @@ +'use strict'; + +const Archetype = require('archetype'); +const authorize = require('../../authorize'); + +function escapeRegExp(str) { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +const DocumentsParams = new Archetype({ + documentId: { + $type: 'string', + $required: true + }, + documentModel: { + $type: 'string' + }, + highlights: { + $type: ['string'] + }, + notes: { + $type: 'string' + } +}).compile('DocumentsParams'); + +const CreateCaseReportParams = new Archetype({ + name: { + $type: 'string', + $required: true + }, + // Array of documents associated with this case report + documents: { + $type: [DocumentsParams] + }, + roles: { + $type: ['string'] + } +}).compile('CreateCaseReportParams'); + +module.exports = ({ db }) => async function createCaseReport(params) { + const { name, documents, roles } = new CreateCaseReportParams(params); + const CaseReport = db.model('__Studio_CaseReport'); + + await authorize('CaseReport.createCaseReport', roles); + + const normalizedName = name.trim(); + + // Count existing case reports with this base name or suffixed with (x) + const base = escapeRegExp(normalizedName); + const namePattern = new RegExp(`^${base}( \\(\\d+\\))?$`); + const existingCount = await CaseReport.countDocuments({ name: { $regex: namePattern } }); + + const finalName = existingCount > 0 ? `${normalizedName} (${existingCount})` : normalizedName; + const docs = Array.isArray(documents) + ? documents + .filter(doc => doc && doc.documentId != null && doc.documentModel) + .map(doc => { + let documentId = doc.documentId; + if (documentId != null && typeof documentId === 'object' && typeof documentId.toString === 'function') { + documentId = documentId.toString(); + } else if (documentId != null) { + documentId = String(documentId); + } + return { + documentId, + documentModel: doc.documentModel, + ...(doc.highlightedFields ? { highlightedFields: doc.highlightedFields } : {}), + ...(doc.notes ? { notes: doc.notes } : {}) + }; + }) + : []; + console.log('document created'); + let created = null; + try { + created = await CaseReport.create({ + name: finalName, + documents: docs + }); + } catch (err) { + console.log('mongoose error', err); + } + console.log('creating the document', created); + + const caseReport = await CaseReport.findById(created._id).lean(); + if (!caseReport) { + throw new Error('Case report created but could not be read back'); + } + + console.log('document queried'); + + console.log('document is lean'); + return { caseReport }; +}; diff --git a/backend/actions/CaseReport/generateCaseReportAISummary.js b/backend/actions/CaseReport/generateCaseReportAISummary.js new file mode 100644 index 00000000..4f793329 --- /dev/null +++ b/backend/actions/CaseReport/generateCaseReportAISummary.js @@ -0,0 +1,82 @@ +'use strict'; + +const Archetype = require('archetype'); +const authorize = require('../../authorize'); +const callLLM = require('../../integrations/callLLM'); +const buildDocumentDataForAISummary = require('./buildDocumentDataForAISummary'); + +const GenerateCaseReportAISummaryParams = new Archetype({ + caseReportId: { + $type: 'string', + $required: true + }, + roles: { + $type: ['string'] + } +}).compile('GenerateCaseReportAISummaryParams'); + +module.exports = ({ db, options }) => async function generateCaseReportAISummary(params) { + const { caseReportId: rawCaseReportId, roles } = new GenerateCaseReportAISummaryParams(params); + const CaseReport = db.model('__Studio_CaseReport'); + + await authorize('CaseReport.generateCaseReportAISummary', roles); + + const caseReportId = rawCaseReportId != null && typeof rawCaseReportId === 'object' && typeof rawCaseReportId.toString === 'function' + ? rawCaseReportId.toString() + : String(rawCaseReportId ?? ''); + if (!caseReportId) { + throw new Error('Case report ID is required'); + } + + const existing = await CaseReport.findById(caseReportId).lean(); + if (!existing) { + throw new Error('Case report not found'); + } + + const summary = typeof existing.summary === 'string' ? existing.summary : ''; + if (!summary.trim()) { + throw new Error('Add a case summary before generating an AI summary.'); + } + + const documents = Array.isArray(existing.documents) ? existing.documents : []; + const documentData = await buildDocumentDataForAISummary(db, documents); + + const documentsContext = documentData.map(doc => { + return `Model: ${doc.model}\nDocument ID: ${doc.documentId}\n${doc.notes ? `Notes: ${doc.notes}\n` : ''}Data: ${JSON.stringify(doc.data, null, 2)}`; + }).join('\n\n---\n\n'); + + const systemPrompt = 'You are a technical writing assistant that improves case report summaries. Your task is to enhance the user\'s summary by incorporating specific details from the document data and investigation notes. Write in clear, professional markdown format. Use the document data to illustrate and support the points made in the summary.'; + + const userPrompt = `Improve and enhance the following case report summary using the document data and investigation notes provided below. Make it more detailed, professional, and well-structured. Use specific examples from the document data to illustrate key points. Format the response as markdown.\n\nUser's Summary:\n${summary}\n\nDocument Data and Investigation Notes:\n${documentsContext}`; + + let aiSummary = null; + try { + const llmResponse = await callLLM( + [{ + role: 'user', + content: [{ + type: 'text', + text: userPrompt + }] + }], + systemPrompt, + options + ); + aiSummary = llmResponse.text; + } catch (err) { + console.error('Error generating AI summary:', err); + throw new Error(err && err.message ? err.message : 'Error generating AI summary'); + } + + const caseReport = await CaseReport.findByIdAndUpdate( + caseReportId, + { AISummary: aiSummary }, + { new: true } + ).lean(); + + if (!caseReport) { + throw new Error('Case report not found'); + } + + return { caseReport, aiSummary }; +}; diff --git a/backend/actions/CaseReport/getCaseReport.js b/backend/actions/CaseReport/getCaseReport.js new file mode 100644 index 00000000..bf13cc0f --- /dev/null +++ b/backend/actions/CaseReport/getCaseReport.js @@ -0,0 +1,28 @@ +'use strict'; + +const Archetype = require('archetype'); +const authorize = require('../../authorize'); + +const GetCaseReportParams = new Archetype({ + caseReportId: { + $type: 'string', + $required: true + }, + roles: { + $type: ['string'] + } +}).compile('GetCaseReportParams'); + +module.exports = ({ db }) => async function getCaseReport(params) { + const { caseReportId, roles } = new GetCaseReportParams(params); + const CaseReport = db.model('__Studio_CaseReport'); + + await authorize('CaseReport.getCaseReports', roles); + + const caseReport = await CaseReport.findById(caseReportId).lean(); + if (!caseReport) { + throw new Error('Case report not found'); + } + + return { caseReport }; +}; diff --git a/backend/actions/CaseReport/getCaseReports.js b/backend/actions/CaseReport/getCaseReports.js new file mode 100644 index 00000000..776ca744 --- /dev/null +++ b/backend/actions/CaseReport/getCaseReports.js @@ -0,0 +1,21 @@ +'use strict'; + +const Archetype = require('archetype'); +const authorize = require('../../authorize'); + +const GetCaseReportsParams = new Archetype({ + roles: { + $type: ['string'] + } +}).compile('GetCaseReportsParams'); + +module.exports = ({ db }) => async function getCaseReports(params) { + const { roles } = new GetCaseReportsParams(params); + const CaseReport = db.model('__Studio_CaseReport'); + + await authorize('CaseReport.getCaseReports', roles); + + const caseReports = await CaseReport.find({}).sort({ createdAt: -1 }).lean(); + + return { caseReports }; +}; diff --git a/backend/actions/CaseReport/index.js b/backend/actions/CaseReport/index.js new file mode 100644 index 00000000..80c8e339 --- /dev/null +++ b/backend/actions/CaseReport/index.js @@ -0,0 +1,7 @@ +'use strict'; + +exports.createCaseReport = require('./createCaseReport'); +exports.getCaseReports = require('./getCaseReports'); +exports.getCaseReport = require('./getCaseReport'); +exports.updateCaseReport = require('./updateCaseReport'); +exports.generateCaseReportAISummary = require('./generateCaseReportAISummary'); diff --git a/backend/actions/CaseReport/updateCaseReport.js b/backend/actions/CaseReport/updateCaseReport.js new file mode 100644 index 00000000..6beef79c --- /dev/null +++ b/backend/actions/CaseReport/updateCaseReport.js @@ -0,0 +1,142 @@ +'use strict'; + +const Archetype = require('archetype'); +const authorize = require('../../authorize'); +const callLLM = require('../../integrations/callLLM'); +const buildDocumentDataForAISummary = require('./buildDocumentDataForAISummary'); + +const UpdateCaseReportParams = new Archetype({ + caseReportId: { + $type: 'string', + $required: true + }, + // Full replacement array of documents for this case report (omit to leave documents unchanged) + documents: { + $type: Array + }, + summary: { + $type: 'string' + }, + status: { + $type: 'string' + }, + roles: { + $type: ['string'] + }, + skipAISummary: { + $type: 'boolean', + $default: false + } +}).compile('UpdateCaseReportParams'); + +module.exports = ({ db, options }) => async function updateCaseReport(params) { + const documentsInRequest = params != null && Object.prototype.hasOwnProperty.call(params, 'documents'); + const paramsForCompile = documentsInRequest ? params : (() => { + const copy = { ...params }; + delete copy.documents; + return copy; + })(); + const { caseReportId: rawCaseReportId, documents, summary, status, roles, skipAISummary } = new UpdateCaseReportParams(paramsForCompile); + const CaseReport = db.model('__Studio_CaseReport'); + + await authorize('CaseReport.updateCaseReport', roles); + + const caseReportId = rawCaseReportId != null && typeof rawCaseReportId === 'object' && typeof rawCaseReportId.toString === 'function' + ? rawCaseReportId.toString() + : String(rawCaseReportId ?? ''); + if (!caseReportId) { + throw new Error('Case report ID is required'); + } + + const docs = documentsInRequest && Array.isArray(documents) + ? documents + .filter(doc => doc && doc.documentId != null && doc.documentModel) + .map(doc => { + // Schema expects documentId as String; normalize to string + let documentId = doc.documentId; + if (documentId != null && typeof documentId === 'object' && typeof documentId.toString === 'function') { + documentId = documentId.toString(); + } else if (documentId != null) { + documentId = String(documentId); + } + return { + documentId, + documentModel: doc.documentModel, + ...(doc.highlightedFields ? { highlightedFields: doc.highlightedFields } : {}), + ...(doc.notes ? { notes: doc.notes } : {}) + }; + }) + : []; + + const updateData = {}; + if (documentsInRequest) { + updateData.documents = docs; + } + let aiSummary = null; + + // If status is explicitly provided, use it + if (status !== undefined) { + updateData.status = status; + } + + if (summary !== undefined) { + updateData.summary = summary; + // If summary is provided and status wasn't explicitly set, set to resolved + if (status === undefined) { + updateData.status = 'resolved'; + } + + // Generate AI summary if summary is provided (unless deferred to generateCaseReportAISummary) + if (summary && summary.trim().length > 0 && !skipAISummary) { + try { + let contextDocEntries = docs; + if (!documentsInRequest || contextDocEntries.length === 0) { + const existingForContext = await CaseReport.findById(caseReportId).lean(); + contextDocEntries = Array.isArray(existingForContext && existingForContext.documents) + ? existingForContext.documents + : []; + } + const documentData = await buildDocumentDataForAISummary(db, contextDocEntries); + + // Build prompt for AI summary + const documentsContext = documentData.map(doc => { + return `Model: ${doc.model}\nDocument ID: ${doc.documentId}\n${doc.notes ? `Notes: ${doc.notes}\n` : ''}Data: ${JSON.stringify(doc.data, null, 2)}`; + }).join('\n\n---\n\n'); + + const systemPrompt = 'You are a technical writing assistant that improves case report summaries. Your task is to enhance the user\'s summary by incorporating specific details from the document data and investigation notes. Write in clear, professional markdown format. Use the document data to illustrate and support the points made in the summary.'; + + const userPrompt = `Improve and enhance the following case report summary using the document data and investigation notes provided below. Make it more detailed, professional, and well-structured. Use specific examples from the document data to illustrate key points. Format the response as markdown.\n\nUser's Summary:\n${summary}\n\nDocument Data and Investigation Notes:\n${documentsContext}`; + + const llmResponse = await callLLM( + [{ + role: 'user', + content: [{ + type: 'text', + text: userPrompt + }] + }], + systemPrompt, + options + ); + + aiSummary = llmResponse.text; + updateData.AISummary = aiSummary; + } catch (err) { + console.error('Error generating AI summary:', err); + // Continue without AI summary if generation fails + } + } + } + + const caseReport = await CaseReport.findByIdAndUpdate( + caseReportId, + updateData, + { new: true } + ).lean(); + + if (!caseReport) { + throw new Error('Case report not found'); + } + + return { caseReport, aiSummary }; +}; diff --git a/backend/actions/index.js b/backend/actions/index.js index eab08bd3..b4f6ee94 100644 --- a/backend/actions/index.js +++ b/backend/actions/index.js @@ -5,5 +5,6 @@ exports.ChatThread = require('./ChatThread'); exports.Dashboard = require('./Dashboard'); exports.Model = require('./Model'); exports.Script = require('./Script'); +exports.CaseReport = require('./CaseReport'); exports.status = require('./status'); exports.Task = require('./Task'); diff --git a/backend/authorize.js b/backend/authorize.js index b42b5b69..089adecc 100644 --- a/backend/authorize.js +++ b/backend/authorize.js @@ -27,9 +27,15 @@ const actionsToRequiredRoles = { 'Model.getEstimatedDocumentCounts': ['owner', 'admin', 'member', 'readonly'], 'Model.getIndexes': ['owner', 'admin', 'member', 'readonly'], 'Model.listModels': ['owner', 'admin', 'member', 'readonly'], + 'Model.updateDocuments': ['owner', 'admin', 'member'], + // CaseReport / Mongoose CaseReport (Bug Hunt) actions + 'CaseReport.createCaseReport': ['owner', 'admin', 'member'], + 'CaseReport.getCaseReports': ['owner', 'admin', 'member', 'readonly'], + 'CaseReport.getCaseReport': ['owner', 'admin', 'member', 'readonly'], + 'CaseReport.updateCaseReport': ['owner', 'admin', 'member'], + 'CaseReport.generateCaseReportAISummary': ['owner', 'admin', 'member'], 'Model.streamDocumentChanges': ['owner', 'admin', 'member', 'readonly'], - 'Model.streamChatMessage': ['owner', 'admin', 'member', 'readonly'], - 'Model.updateDocuments': ['owner', 'admin', 'member'] + 'Model.streamChatMessage': ['owner', 'admin', 'member', 'readonly'] }; module.exports = function authorize(action, roles) { diff --git a/backend/db/caseReportSchema.js b/backend/db/caseReportSchema.js new file mode 100644 index 00000000..2adb9f75 --- /dev/null +++ b/backend/db/caseReportSchema.js @@ -0,0 +1,40 @@ +'use strict'; + +const mongoose = require('mongoose'); + +const caseReportSchema = new mongoose.Schema({ + name: { + type: String, + required: true + }, + status: { + type: String, + $required: true, + default: 'created', + enum: ['created', 'in_progress', 'cancelled', 'resolved', 'archived'] + }, + documents: [{ + documentId: { + type: String, // for cases where its not an objectId but is used like one + refPath: 'documents.documentModel' + }, + highlightedFields: [String], + documentModel: { + type: String, + required: true + }, + notes: { + type: String + } + }], + summary: { + type: String + }, + AISummary: { + type: String + } +}, { + timestamps: true +}); + +module.exports = caseReportSchema; diff --git a/backend/index.js b/backend/index.js index 56457cf0..1f9095f2 100644 --- a/backend/index.js +++ b/backend/index.js @@ -7,6 +7,7 @@ const mongoose = require('mongoose'); const chatMessageSchema = require('./db/chatMessageSchema'); const chatThreadSchema = require('./db/chatThreadSchema'); const dashboardSchema = require('./db/dashboardSchema'); +const caseReportSchema = require('./db/caseReportSchema'); const dashboardResultSchema = require('./db/dashboardResultSchema'); module.exports = function backend(db, studioConnection, options) { @@ -17,6 +18,7 @@ module.exports = function backend(db, studioConnection, options) { const DashboardResult = studioConnection.model('__Studio_DashboardResult', dashboardResultSchema, 'studio__dashboardResults'); const ChatMessage = studioConnection.model('__Studio_ChatMessage', chatMessageSchema, 'studio__chatMessages'); const ChatThread = studioConnection.model('__Studio_ChatThread', chatThreadSchema, 'studio__chatThreads'); + const CaseReport = studioConnection.model('__Studio_CaseReport', caseReportSchema, 'studio__caseReports'); let changeStream = null; if (options?.changeStream) { diff --git a/frontend/src/api.js b/frontend/src/api.js index 3aae8371..6b9945c3 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -78,6 +78,23 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) { return client.post('', { action: 'ChatMessage.executeScript', ...params }).then(res => res.data); } }; + exports.CaseReport = { + createCaseReport(params) { + return client.post('', { action: 'CaseReport.createCaseReport', ...params }).then(res => res.data); + }, + getCaseReports(params) { + return client.post('', { action: 'CaseReport.getCaseReports', ...params }).then(res => res.data); + }, + getCaseReport(params) { + return client.post('', { action: 'CaseReport.getCaseReport', ...params }).then(res => res.data); + }, + updateCaseReport(params) { + return client.post('', { action: 'CaseReport.updateCaseReport', ...params }).then(res => res.data); + }, + generateCaseReportAISummary(params) { + return client.post('', { action: 'CaseReport.generateCaseReportAISummary', ...params }).then(res => res.data); + } + }; exports.Model = { addField(params) { return client.post('', { action: 'Model.addField', ...params }).then(res => res.data); @@ -299,6 +316,23 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) { return client.post('/ChatMessage/executeScript', params).then(res => res.data); } }; + exports.CaseReport = { + createCaseReport: function createCaseReport(params) { + return client.post('/CaseReport/createCaseReport', params).then(res => res.data); + }, + getCaseReports: function getCaseReports(params) { + return client.post('/CaseReport/getCaseReports', params).then(res => res.data); + }, + getCaseReport: function getCaseReport(params) { + return client.post('/CaseReport/getCaseReport', params).then(res => res.data); + }, + updateCaseReport: function updateCaseReport(params) { + return client.post('/CaseReport/updateCaseReport', params).then(res => res.data); + }, + generateCaseReportAISummary: function generateCaseReportAISummary(params) { + return client.post('/CaseReport/generateCaseReportAISummary', params).then(res => res.data); + } + }; exports.Model = { addField(params) { return client.post('/Model/addField', params).then(res => res.data); diff --git a/frontend/src/case-reports/case-reports.html b/frontend/src/case-reports/case-reports.html new file mode 100644 index 00000000..462cb8a4 --- /dev/null +++ b/frontend/src/case-reports/case-reports.html @@ -0,0 +1,135 @@ +
+
+ + + + +
+
+
+

No case reports yet

+

Go to Documents and open the Sleuth panel on the right to create a case report.

+
+ + Go to Documents + +
+
+
+ +
+
+
+

Case Reports

+
+
+ + Go to Documents + +
+
+
+
+
+ + + + + + + + + + + + + + + + + +
NameStatusCreated +
{{caseReport.name}} + + {{ formatStatus(caseReport.status) }} + + + {{ formatDate(caseReport.createdAt) }} + +
+ + + + Open + +
+
+
+
+
+
+ + + + +
diff --git a/frontend/src/case-reports/case-reports.js b/frontend/src/case-reports/case-reports.js new file mode 100644 index 00000000..c142970a --- /dev/null +++ b/frontend/src/case-reports/case-reports.js @@ -0,0 +1,121 @@ +'use strict'; + +const api = require('../api'); +const template = require('./case-reports.html'); + +module.exports = app => app.component('case-reports', { + template: template, + data: () => ({ + status: 'loading', + caseReports: [], + showConfirmModal: false, + confirmAction: null, + confirmCaseReportId: null, + confirmMessage: '', + confirmButtonText: '', + confirmButtonClass: '' + }), + methods: { + formatDate(date) { + if (!date) return 'N/A'; + try { + const d = new Date(date); + if (isNaN(d.getTime())) return 'N/A'; + return d.toLocaleDateString() + ' ' + d.toLocaleTimeString(); + } catch (e) { + return 'N/A'; + } + }, + formatStatus(status) { + if (!status) return 'Unknown'; + const statusMap = { + created: 'Created', + in_progress: 'In Progress', + cancelled: 'Cancelled', + resolved: 'Resolved', + archived: 'Archived' + }; + return statusMap[status] || status; + }, + getStatusClass(status) { + if (!status) return 'bg-gray-100 text-gray-800'; + const classMap = { + created: 'bg-blue-100 text-blue-800', + in_progress: 'bg-yellow-100 text-yellow-800', + cancelled: 'bg-red-100 text-red-800', + resolved: 'bg-green-100 text-green-800', + archived: 'bg-gray-100 text-gray-800' + }; + return classMap[status] || 'bg-gray-100 text-gray-800'; + }, + openCancelConfirm(caseReportId) { + this.confirmCaseReportId = caseReportId; + this.confirmAction = 'cancel'; + this.confirmMessage = 'Are you sure you want to cancel this case report?'; + this.confirmButtonText = 'Cancel Case Report'; + this.confirmButtonClass = 'bg-red-600 hover:bg-red-500 focus-visible:outline-red-600'; + this.showConfirmModal = true; + }, + openArchiveConfirm(caseReportId) { + this.confirmCaseReportId = caseReportId; + this.confirmAction = 'archive'; + this.confirmMessage = 'Are you sure you want to archive this case report?'; + this.confirmButtonText = 'Archive Case Report'; + this.confirmButtonClass = 'bg-gray-600 hover:bg-gray-500 focus-visible:outline-gray-600'; + this.showConfirmModal = true; + }, + closeConfirmModal() { + this.showConfirmModal = false; + this.confirmCaseReportId = null; + this.confirmAction = null; + this.confirmMessage = ''; + this.confirmButtonText = ''; + this.confirmButtonClass = ''; + }, + async executeConfirmAction() { + if (!this.confirmCaseReportId || !this.confirmAction) { + return; + } + + try { + let status; + let successMessage; + + if (this.confirmAction === 'cancel') { + status = 'cancelled'; + successMessage = 'Case report cancelled'; + } else if (this.confirmAction === 'archive') { + status = 'archived'; + successMessage = 'Case report archived'; + } else { + return; + } + + await api.CaseReport.updateCaseReport({ + caseReportId: this.confirmCaseReportId, + status + }); + + this.$toast.success(successMessage); + this.closeConfirmModal(); + + // Reload case reports + const { caseReports } = await api.CaseReport.getCaseReports(); + this.caseReports = caseReports; + } catch (error) { + console.error(`Error ${this.confirmAction}ing case report`, error); + this.$toast.error(error?.message || `Error ${this.confirmAction}ing case report`); + } + } + }, + async mounted() { + try { + const { caseReports } = await api.CaseReport.getCaseReports(); + this.caseReports = caseReports; + this.status = 'loaded'; + } catch (error) { + console.error('Error loading case reports', error); + this.status = 'loaded'; + } + } +}); diff --git a/frontend/src/document/document.html b/frontend/src/document/document.html index 4208dafa..28f3076a 100644 --- a/frontend/src/document/document.html +++ b/frontend/src/document/document.html @@ -143,6 +143,14 @@ > Clone + @@ -245,6 +253,14 @@ > Clone + diff --git a/frontend/src/document/document.js b/frontend/src/document/document.js index 518dc6eb..db2a1bf0 100644 --- a/frontend/src/document/document.js +++ b/frontend/src/document/document.js @@ -3,6 +3,7 @@ const api = require('../api'); const mpath = require('mpath'); const template = require('./document.html'); +const { hasAccess } = require('../routes'); const appendCSS = require('../appendCSS'); @@ -80,15 +81,8 @@ module.exports = app => app.component('document', { canEdit() { return this.canManipulate && this.viewMode === 'fields'; }, - keyboardShortcuts() { - const shortcuts = []; - if (this.editting && this.canManipulate) { - shortcuts.push({ command: 'Ctrl + S', description: 'Save document' }); - } - return shortcuts; - }, - isLambda() { - return !!window?.MONGOOSE_STUDIO_CONFIG?.isLambda; + hasSleuthAccess() { + return hasAccess(this.roles, 'mongoose-sleuth'); } }, watch: { @@ -99,6 +93,29 @@ module.exports = app => app.component('document', { } }, methods: { + addToSleuth() { + try { + if (typeof window !== 'undefined' && window.sessionStorage) { + window.sessionStorage.setItem('studio:sleuth:addDocument', JSON.stringify({ + model: this.model, + documentId: this.documentId + })); + } + this.$router.push({ path: `/model/${encodeURIComponent(this.model)}`, query: { openSleuth: '1' } }); + } catch (e) { + console.error('Add to Sleuth', e); + } + }, + keyboardShortcuts() { + const shortcuts = []; + if (this.editting && this.canManipulate) { + shortcuts.push({ command: 'Ctrl + S', description: 'Save document' }); + } + return shortcuts; + }, + isLambda() { + return !!window?.MONGOOSE_STUDIO_CONFIG?.isLambda; + }, handleSaveShortcut(event) { const key = typeof event?.key === 'string' ? event.key.toLowerCase() : ''; const isSaveShortcut = (event.ctrlKey || event.metaKey) && key === 's'; diff --git a/frontend/src/models/models.css b/frontend/src/models/models.css index ac594a8f..e3c9840f 100644 --- a/frontend/src/models/models.css +++ b/frontend/src/models/models.css @@ -1,16 +1,21 @@ .models { - position: relative; display: flex; flex-direction: row; - min-height: calc(100% - 56px); + height: calc(100vh - 55px); +} + +.models > aside { + height: 100%; + overflow-y: auto; } .models .documents { - flex-grow: 1; - overflow: visible; - max-height: calc(100vh - 56px); + flex: 1 1 auto; + min-width: 0; + overflow: hidden; display: flex; flex-direction: column; + height: 100%; } .models .documents-container { diff --git a/frontend/src/models/models.html b/frontend/src/models/models.html index 4e11ce1b..8bd8d4a3 100644 --- a/frontend/src/models/models.html +++ b/frontend/src/models/models.html @@ -1,4 +1,4 @@ -
+