diff --git a/backend/.gitignore b/backend/.gitignore index 3fa91ea..de5fee4 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1,3 +1,4 @@ node_modules/ .env database.sqlite +public/uploads/ diff --git a/backend/index.js b/backend/index.js index b1f14c8..b5d3a9d 100644 --- a/backend/index.js +++ b/backend/index.js @@ -1,4 +1,5 @@ const express = require("express"); +const path = require('path'); const { Sequelize } = require("sequelize"); const app = express(); @@ -13,6 +14,9 @@ app.use(cors(corsOptions)); // enable json middleware app.use(express.json()); +// Specify the directory to serve static files +app.use(express.static(path.join(__dirname, "public"))); + // init sequalize const sequelize = new Sequelize({ dialect: "sqlite", diff --git a/backend/migrations/20240316200815-create-attachments.js b/backend/migrations/20240316200815-create-attachments.js new file mode 100644 index 0000000..489f59c --- /dev/null +++ b/backend/migrations/20240316200815-create-attachments.js @@ -0,0 +1,38 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up (queryInterface, Sequelize) { + await queryInterface.createTable('attachments', { + id: { + type: Sequelize.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + title: { + type: Sequelize.STRING, + allowNull: false, + }, + detail: { + type: Sequelize.STRING, + allowNull: true, + }, + path: { + type: Sequelize.STRING, + allowNull: false, + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE + } + }); + }, + + async down (queryInterface, Sequelize) { + await queryInterface.dropTable('attachments'); + } +}; diff --git a/backend/migrations/20240316201158-create-cases-attachments.js b/backend/migrations/20240316201158-create-cases-attachments.js new file mode 100644 index 0000000..96b7c73 --- /dev/null +++ b/backend/migrations/20240316201158-create-cases-attachments.js @@ -0,0 +1,48 @@ +"use strict"; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable("caseAttachments", { + id: { + type: Sequelize.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + caseId: { + type: Sequelize.INTEGER, + references: { + model: "cases", + key: "id", + }, + onUpdate: "CASCADE", + onDelete: "CASCADE", + }, + attachmentId: { + type: Sequelize.INTEGER, + references: { + model: "attachments", + key: "id", + }, + onUpdate: "CASCADE", + onDelete: "CASCADE", + }, + createdAt: { + type: Sequelize.DATE, + allowNull: false, + }, + updatedAt: { + type: Sequelize.DATE, + allowNull: false, + }, + }); + + await queryInterface.addIndex("caseAttachments", ["caseId", "attachmentId"], { + unique: true, + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable("caseAttachments"); + }, +}; diff --git a/backend/models/attachments.js b/backend/models/attachments.js new file mode 100644 index 0000000..92ca26d --- /dev/null +++ b/backend/models/attachments.js @@ -0,0 +1,26 @@ +function defineAttachment(sequelize, DataTypes) { + const Attachment = sequelize.define("Attachment", { + title: { + type: DataTypes.STRING, + allowNull: false, + }, + detail: { + type: DataTypes.STRING, + allowNull: true, + }, + path: { + type: DataTypes.STRING, + allowNull: false, + }, + }); + + Attachment.associate = (models) => { + Attachment.belongsToMany(models.Case, { + through: "caseAttachments" + }); + }; + + return Attachment; +} + +module.exports = defineAttachment; diff --git a/backend/models/caseAttachments.js b/backend/models/caseAttachments.js new file mode 100644 index 0000000..898565f --- /dev/null +++ b/backend/models/caseAttachments.js @@ -0,0 +1,27 @@ +function defineCaseAttachment(sequelize, DataTypes) { + const CaseAttachment = sequelize.define("CaseAttachment", { + caseId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + attachmentId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + }); + + CaseAttachment.associate = (models) => { + CaseAttachment.belongsTo(models.Case, { + foreignKey: "caseId", + onDelete: "CASCADE", + }); + CaseAttachment.belongsTo(models.Attachment, { + foreignKey: "attachmentId", + onDelete: "CASCADE", + }); + }; + + return CaseAttachment; +} + +module.exports = defineCaseAttachment; diff --git a/backend/public/sample/861px-Selenium_Logo.png b/backend/public/sample/861px-Selenium_Logo.png new file mode 100644 index 0000000..9f4242c Binary files /dev/null and b/backend/public/sample/861px-Selenium_Logo.png differ diff --git a/backend/public/sample/logo-shadow.svg b/backend/public/sample/logo-shadow.svg new file mode 100644 index 0000000..e5b59bb --- /dev/null +++ b/backend/public/sample/logo-shadow.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/routes/cases/index.js b/backend/routes/cases/index.js index 64bdd40..9e1050b 100644 --- a/backend/routes/cases/index.js +++ b/backend/routes/cases/index.js @@ -2,13 +2,17 @@ const express = require("express"); const router = express.Router(); const defineCase = require("../../models/cases"); const defineStep = require("../../models/steps"); +const defineAttachment = require("../../models/attachments"); const { DataTypes } = require("sequelize"); module.exports = function (sequelize) { const Case = defineCase(sequelize, DataTypes); const Step = defineStep(sequelize, DataTypes); - Case.belongsToMany(Step, { through: 'caseSteps' }); - Step.belongsToMany(Case, { through: 'caseSteps' }); + const Attachment = defineAttachment(sequelize, DataTypes); + Case.belongsToMany(Step, { through: "caseSteps" }); + Step.belongsToMany(Case, { through: "caseSteps" }); + Case.belongsToMany(Attachment, { through: "caseAttachments" }); + Attachment.belongsToMany(Case, { through: "caseAttachments" }); router.get("/", async (req, res) => { const { caseId, folderId } = req.query; @@ -20,10 +24,15 @@ module.exports = function (sequelize) { if (caseId) { // Include steps if requested using caseId const testcase = await Case.findByPk(caseId, { - include: [{ - model: Step, - through: { attributes: ['stepNo'] }, - }], + include: [ + { + model: Step, + through: { attributes: ["stepNo"] }, + }, + { + model: Attachment, + }, + ], }); return res.json(testcase); } diff --git a/backend/seeders/seed.js b/backend/seeders/seed.js index 635f57d..dc50257 100644 --- a/backend/seeders/seed.js +++ b/backend/seeders/seed.js @@ -174,6 +174,38 @@ module.exports = { updatedAt: new Date(), }, ]); + + await queryInterface.bulkInsert("attachments", [ + { + title: "Selenium logo", + detail: "", + path: "http://localhost:3001/sample/861px-Selenium_Logo.png", + createdAt: new Date(), + updatedAt: new Date(), + }, + { + title: "vitest logo", + detail: "", + path: "http://localhost:3001/sample/logo-shadow.svg", + createdAt: new Date(), + updatedAt: new Date(), + } + ]); + + await queryInterface.bulkInsert("caseAttachments", [ + { + caseId: 1, + attachmentId: 1, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + caseId: 1, + attachmentId: 2, + createdAt: new Date(), + updatedAt: new Date(), + } + ]); }, down: async (queryInterface, Sequelize) => { diff --git a/frontend/app/projects/[projectId]/folders/[folderId]/cases/[caseId]/attachments-editor.tsx b/frontend/app/projects/[projectId]/folders/[folderId]/cases/[caseId]/attachments-editor.tsx new file mode 100644 index 0000000..8b3aa04 --- /dev/null +++ b/frontend/app/projects/[projectId]/folders/[folderId]/cases/[caseId]/attachments-editor.tsx @@ -0,0 +1,35 @@ +import { Image, Button, Tooltip } from "@nextui-org/react"; +import { AttachmentType } from "./page"; +import { Trash } from "lucide-react"; + +type Props = { + attachments: AttachmentType[]; + onAttachmentDelete: (attachmentId: number) => void; +}; + +export default function AttachmentsEditor({ + attachments = [], + onAttachmentDelete, +}: Props) { + return ( + <> + {attachments.map((attachment, index) => ( +
+ {attachment.title} +
+ + + +
+
+ ))} + + ); +} diff --git a/frontend/app/projects/[projectId]/folders/[folderId]/cases/[caseId]/page.tsx b/frontend/app/projects/[projectId]/folders/[folderId]/cases/[caseId]/page.tsx index 9cf6c6a..5ad979e 100644 --- a/frontend/app/projects/[projectId]/folders/[folderId]/cases/[caseId]/page.tsx +++ b/frontend/app/projects/[projectId]/folders/[folderId]/cases/[caseId]/page.tsx @@ -12,6 +12,7 @@ import { import { Plus, ArrowUpFromLine } from "lucide-react"; import { priorities, testTypes, templates } from "@/config/selection"; import StepsEditor from "./steps-editor"; +import AttachmentsEditor from "./attachments-editor"; import Config from "@/config/config"; const apiServer = Config.apiServer; @@ -28,6 +29,7 @@ type CaseType = { expectedResults: string; folderId: number; Steps: StepType[]; + Attachments: AttachmentType[]; }; type CaseStepType = { @@ -46,6 +48,23 @@ export type StepType = { caseSteps: CaseStepType; }; +type CaseAttachmentType = { + createdAt: Date; + updatedAt: Date; + CaseId: number; + AttachmentId: number; +}; + +export type AttachmentType = { + id: number; + title: string; + detail: string; + path: string; + createdAt: Date; + updatedAt: Date; + caseSteps: CaseAttachmentType; +}; + const defaultTestCase = { id: 0, title: "", @@ -411,6 +430,10 @@ export default function Page({
Attachments
+ console.log(id)} + />