Merge pull request #286 from kimatata/develop

This commit is contained in:
kimatata
2025-09-23 11:39:42 +09:00
committed by GitHub
100 changed files with 2903 additions and 2527 deletions

View File

@@ -61,11 +61,11 @@ COPY backend/package.json backend/package-lock.json* ./
RUN npm ci RUN npm ci
WORKDIR /app WORKDIR /app
# Copy custom combined_server.js that combines frontend and backend # Copy custom entrypoint.js that combines frontend and backend
COPY combined_server.js ./ COPY entrypoint.js ./
# Expose the port # Expose the port
EXPOSE 8000 EXPOSE 8000
# Run database migrations and start the combined server # Run database migrations and start the combined server
CMD ["node", "combined_server.js"] CMD ["node", "entrypoint.js"]

View File

@@ -1,7 +1,12 @@
const path = require('path'); import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const databasePath = path.resolve(__dirname, '../database/database.sqlite'); const databasePath = path.resolve(__dirname, '../database/database.sqlite');
module.exports = { export default {
development: { development: {
dialect: 'sqlite', dialect: 'sqlite',
storage: databasePath, storage: databasePath,

View File

@@ -1,5 +1,4 @@
const server = require('./server'); import server from './server.js';
const PORT = process.env.PORT || 8001; const PORT = process.env.PORT || 8001;
server.listen(PORT, () => { server.listen(PORT, () => {

View File

@@ -1,9 +1,9 @@
const jwt = require('jsonwebtoken'); import jwt from 'jsonwebtoken';
const { DataTypes } = require('sequelize'); import { DataTypes } from 'sequelize';
const { roles, defaultDangerKey } = require('../routes/users/authSettings'); import { roles, defaultDangerKey } from '../routes/users/authSettings.js';
const defineUser = require('../models/users'); import defineUser from '../models/users.js';
function authMiddleware(sequelize) { export default function authMiddleware(sequelize) {
/** /**
* Verify user sined in * Verify user sined in
* *
@@ -53,5 +53,3 @@ function authMiddleware(sequelize) {
verifyAdmin, verifyAdmin,
}; };
} }
module.exports = authMiddleware;

View File

@@ -1,12 +1,12 @@
const { DataTypes } = require('sequelize'); import { DataTypes } from 'sequelize';
const { memberRoles } = require('../routes/users/authSettings'); import { memberRoles } from '../routes/users/authSettings.js';
const defineMember = require('../models/members'); import defineMember from '../models/members.js';
const defineProject = require('../models/projects'); import defineProject from '../models/projects.js';
const defineFolder = require('../models/folders'); import defineFolder from '../models/folders.js';
const defineCase = require('../models/cases'); import defineCase from '../models/cases.js';
const defineRun = require('../models/runs'); import defineRun from '../models/runs.js';
function verifyEditableMiddleware(sequelize) { export default function verifyEditableMiddleware(sequelize) {
/** /**
* Verify user has project * Verify user has project
* (have to be called after verifySignedIn() middleware) * (have to be called after verifySignedIn() middleware)
@@ -291,5 +291,3 @@ function verifyEditableMiddleware(sequelize) {
verifyProjectReporterFromRunId, verifyProjectReporterFromRunId,
}; };
} }
module.exports = verifyEditableMiddleware;

View File

@@ -1,11 +1,11 @@
const { DataTypes } = require('sequelize'); import { DataTypes } from 'sequelize';
const defineMember = require('../models/members'); import defineMember from '../models/members.js';
const defineProject = require('../models/projects'); import defineProject from '../models/projects.js';
const defineFolder = require('../models/folders'); import defineFolder from '../models/folders.js';
const defineCase = require('../models/cases'); import defineCase from '../models/cases.js';
const defineRun = require('../models/runs'); import defineRun from '../models/runs.js';
function verifyVisibleMiddleware(sequelize) { export default function verifyVisibleMiddleware(sequelize) {
/** /**
* Verify user can read project by projectId * Verify user can read project by projectId
* (have to be called after verifySignedIn() middleware) * (have to be called after verifySignedIn() middleware)
@@ -160,5 +160,3 @@ function verifyVisibleMiddleware(sequelize) {
verifyProjectVisibleFromRunId, verifyProjectVisibleFromRunId,
}; };
} }
module.exports = verifyVisibleMiddleware;

View File

@@ -1,8 +1,4 @@
'use strict'; export async function up(queryInterface, Sequelize) {
/** @type {import('sequelize-cli').Migration} */
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('users', { await queryInterface.createTable('users', {
id: { id: {
type: Sequelize.INTEGER, type: Sequelize.INTEGER,
@@ -38,9 +34,8 @@ module.exports = {
allowNull: false, allowNull: false,
}, },
}); });
}, }
down: async (queryInterface) => { export async function down(queryInterface) {
await queryInterface.dropTable('users'); await queryInterface.dropTable('users');
}, }
};

View File

@@ -1,8 +1,4 @@
'use strict'; export async function up(queryInterface, Sequelize) {
/** @type {import('sequelize-cli').Migration} */
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('projects', { await queryInterface.createTable('projects', {
id: { id: {
type: Sequelize.INTEGER, type: Sequelize.INTEGER,
@@ -40,9 +36,8 @@ module.exports = {
allowNull: false, allowNull: false,
}, },
}); });
}, }
down: async (queryInterface) => { export async function down(queryInterface) {
await queryInterface.dropTable('projects'); await queryInterface.dropTable('projects');
}, }
};

View File

@@ -1,8 +1,4 @@
'use strict'; export async function up(queryInterface, Sequelize) {
/** @type {import('sequelize-cli').Migration} */
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('folders', { await queryInterface.createTable('folders', {
id: { id: {
type: Sequelize.INTEGER, type: Sequelize.INTEGER,
@@ -40,9 +36,8 @@ module.exports = {
allowNull: false, allowNull: false,
}, },
}); });
}, }
async down(queryInterface) { export async function down(queryInterface) {
await queryInterface.dropTable('folders'); await queryInterface.dropTable('folders');
}, }
};

View File

@@ -1,8 +1,4 @@
'use strict'; export async function up(queryInterface, Sequelize) {
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('runs', { await queryInterface.createTable('runs', {
id: { id: {
type: Sequelize.INTEGER, type: Sequelize.INTEGER,
@@ -44,9 +40,8 @@ module.exports = {
type: Sequelize.DATE, type: Sequelize.DATE,
}, },
}); });
}, }
async down(queryInterface) { export async function down(queryInterface) {
await queryInterface.dropTable('runs'); await queryInterface.dropTable('runs');
}, }
};

View File

@@ -1,8 +1,4 @@
'use strict'; export async function up(queryInterface, Sequelize) {
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('cases', { await queryInterface.createTable('cases', {
id: { id: {
type: Sequelize.INTEGER, type: Sequelize.INTEGER,
@@ -64,9 +60,8 @@ module.exports = {
type: Sequelize.DATE, type: Sequelize.DATE,
}, },
}); });
}, }
async down(queryInterface) { export async function down(queryInterface) {
await queryInterface.dropTable('cases'); await queryInterface.dropTable('cases');
}, }
};

View File

@@ -1,8 +1,4 @@
'use strict'; export async function up(queryInterface, Sequelize) {
/** @type {import('sequelize-cli').Migration} */
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('steps', { await queryInterface.createTable('steps', {
id: { id: {
type: Sequelize.INTEGER, type: Sequelize.INTEGER,
@@ -26,9 +22,8 @@ module.exports = {
allowNull: false, allowNull: false,
}, },
}); });
}, }
down: async (queryInterface) => { export async function down(queryInterface) {
await queryInterface.dropTable('steps'); await queryInterface.dropTable('steps');
}, }
};

View File

@@ -1,8 +1,4 @@
'use strict'; export async function up(queryInterface, Sequelize) {
/** @type {import('sequelize-cli').Migration} */
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('caseSteps', { await queryInterface.createTable('caseSteps', {
id: { id: {
type: Sequelize.INTEGER, type: Sequelize.INTEGER,
@@ -44,9 +40,8 @@ module.exports = {
await queryInterface.addIndex('caseSteps', ['caseId', 'stepId'], { await queryInterface.addIndex('caseSteps', ['caseId', 'stepId'], {
unique: true, unique: true,
}); });
}, }
down: async (queryInterface) => { export async function down(queryInterface) {
await queryInterface.dropTable('caseSteps'); await queryInterface.dropTable('caseSteps');
}, }
};

View File

@@ -1,8 +1,4 @@
'use strict'; export async function up(queryInterface, Sequelize) {
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('attachments', { await queryInterface.createTable('attachments', {
id: { id: {
type: Sequelize.INTEGER, type: Sequelize.INTEGER,
@@ -30,9 +26,8 @@ module.exports = {
type: Sequelize.DATE, type: Sequelize.DATE,
}, },
}); });
}, }
async down(queryInterface) { export async function down(queryInterface) {
await queryInterface.dropTable('attachments'); await queryInterface.dropTable('attachments');
}, }
};

View File

@@ -1,8 +1,4 @@
'use strict'; export async function up(queryInterface, Sequelize) {
/** @type {import('sequelize-cli').Migration} */
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('caseAttachments', { await queryInterface.createTable('caseAttachments', {
id: { id: {
type: Sequelize.INTEGER, type: Sequelize.INTEGER,
@@ -40,9 +36,8 @@ module.exports = {
await queryInterface.addIndex('caseAttachments', ['caseId', 'attachmentId'], { await queryInterface.addIndex('caseAttachments', ['caseId', 'attachmentId'], {
unique: true, unique: true,
}); });
}, }
down: async (queryInterface) => { export async function down(queryInterface) {
await queryInterface.dropTable('caseAttachments'); await queryInterface.dropTable('caseAttachments');
}, }
};

View File

@@ -1,8 +1,4 @@
'use strict'; export async function up(queryInterface, Sequelize) {
/** @type {import('sequelize-cli').Migration} */
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('runCases', { await queryInterface.createTable('runCases', {
id: { id: {
type: Sequelize.INTEGER, type: Sequelize.INTEGER,
@@ -44,9 +40,8 @@ module.exports = {
await queryInterface.addIndex('runCases', ['runId', 'caseId'], { await queryInterface.addIndex('runCases', ['runId', 'caseId'], {
unique: true, unique: true,
}); });
}, }
down: async (queryInterface) => { export async function down(queryInterface) {
await queryInterface.dropTable('runCases'); await queryInterface.dropTable('runCases');
}, }
};

View File

@@ -1,8 +1,4 @@
'use strict'; export async function up(queryInterface, Sequelize) {
/** @type {import('sequelize-cli').Migration} */
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('members', { await queryInterface.createTable('members', {
id: { id: {
type: Sequelize.INTEGER, type: Sequelize.INTEGER,
@@ -44,9 +40,8 @@ module.exports = {
await queryInterface.addIndex('members', ['userId', 'projectId'], { await queryInterface.addIndex('members', ['userId', 'projectId'], {
unique: true, unique: true,
}); });
}, }
down: async (queryInterface) => { export async function down(queryInterface) {
await queryInterface.dropTable('members'); await queryInterface.dropTable('members');
}, }
};

View File

@@ -1,8 +1,4 @@
'use strict'; export async function up(queryInterface) {
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface) {
/** /**
* Add altering commands here. * Add altering commands here.
* *
@@ -13,14 +9,13 @@ module.exports = {
await queryInterface.renameColumn('users', 'createdAt', 'created_at'); await queryInterface.renameColumn('users', 'createdAt', 'created_at');
await queryInterface.renameColumn('users', 'updatedAt', 'updated_at'); await queryInterface.renameColumn('users', 'updatedAt', 'updated_at');
await queryInterface.renameColumn('users', 'avatarPath', 'avatar_path'); await queryInterface.renameColumn('users', 'avatarPath', 'avatar_path');
}, }
async down() { export async function down() {
/** /**
* Add reverting commands here. * Add reverting commands here.
* *
* Example: * Example:
* await queryInterface.dropTable('users'); * await queryInterface.dropTable('users');
*/ */
}, }
};

View File

@@ -1,13 +1,9 @@
'use strict'; export async function up(queryInterface) {
module.exports = {
up: async (queryInterface) => {
// Rename column 'path' to 'filename' in 'Attachments' table // Rename column 'path' to 'filename' in 'Attachments' table
await queryInterface.renameColumn('Attachments', 'path', 'filename'); await queryInterface.renameColumn('Attachments', 'path', 'filename');
}, }
down: async (queryInterface) => { export async function down(queryInterface) {
// Revert column name from 'filename' back to 'path' // Revert column name from 'filename' back to 'path'
await queryInterface.renameColumn('Attachments', 'filename', 'path'); await queryInterface.renameColumn('Attachments', 'filename', 'path');
}, }
};

View File

@@ -1,8 +1,4 @@
'use strict'; export async function up(queryInterface) {
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface) {
await queryInterface.addConstraint('folders', { await queryInterface.addConstraint('folders', {
fields: ['parentFolderId'], fields: ['parentFolderId'],
type: 'foreign key', type: 'foreign key',
@@ -14,9 +10,8 @@ module.exports = {
onDelete: 'CASCADE', onDelete: 'CASCADE',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',
}); });
}, }
async down(queryInterface) { export async function down(queryInterface) {
await queryInterface.removeConstraint('folders', 'fk_folders_parentFolderId'); await queryInterface.removeConstraint('folders', 'fk_folders_parentFolderId');
}, }
};

View File

@@ -23,4 +23,4 @@ function defineAttachment(sequelize, DataTypes) {
return Attachment; return Attachment;
} }
module.exports = defineAttachment; export default defineAttachment;

View File

@@ -24,4 +24,4 @@ function defineCaseAttachment(sequelize, DataTypes) {
return CaseAttachment; return CaseAttachment;
} }
module.exports = defineCaseAttachment; export default defineCaseAttachment;

View File

@@ -28,4 +28,4 @@ function defineCaseStep(sequelize, DataTypes) {
return CaseStep; return CaseStep;
} }
module.exports = defineCaseStep; export default defineCaseStep;

View File

@@ -60,4 +60,4 @@ function defineCase(sequelize, DataTypes) {
return Case; return Case;
} }
module.exports = defineCase; export default defineCase;

View File

@@ -37,4 +37,4 @@ function defineFolder(sequelize, DataTypes) {
return Folder; return Folder;
} }
module.exports = defineFolder; export default defineFolder;

View File

@@ -1,12 +1,13 @@
'use strict'; import fs from 'fs';
import path from 'path';
const fs = require('fs'); import { fileURLToPath } from 'url';
const path = require('path'); import process from 'process';
const process = require('process'); import Sequelize from 'sequelize';
const Sequelize = require('sequelize'); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const basename = path.basename(__filename); const basename = path.basename(__filename);
const env = process.env.NODE_ENV || 'development'; import configDefault from '../config/config';
const config = require(__dirname + '/../config/config.json')[env]; const config = configDefault[process.env.NODE_ENV || 'development'];
const db = {}; const db = {};
let sequelize; let sequelize;
@@ -16,15 +17,16 @@ if (config.use_env_variable) {
sequelize = new Sequelize(config.database, config.username, config.password, config); sequelize = new Sequelize(config.database, config.username, config.password, config);
} }
fs.readdirSync(__dirname) const modelFiles = fs.readdirSync(__dirname).filter((file) => {
.filter((file) => {
return file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js' && file.indexOf('.test.js') === -1; return file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js' && file.indexOf('.test.js') === -1;
})
.forEach((file) => {
const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes);
db[model.name] = model;
}); });
for (const file of modelFiles) {
const module = await import(path.join(__dirname, file));
const model = module.default(sequelize, Sequelize.DataTypes);
db[model.name] = model;
}
Object.keys(db).forEach((modelName) => { Object.keys(db).forEach((modelName) => {
if (db[modelName].associate) { if (db[modelName].associate) {
db[modelName].associate(db); db[modelName].associate(db);
@@ -34,4 +36,4 @@ Object.keys(db).forEach((modelName) => {
db.sequelize = sequelize; db.sequelize = sequelize;
db.Sequelize = Sequelize; db.Sequelize = Sequelize;
module.exports = db; export default db;

View File

@@ -28,4 +28,4 @@ function defineMember(sequelize, DataTypes) {
return Member; return Member;
} }
module.exports = defineMember; export default defineMember;

View File

@@ -32,4 +32,4 @@ function defineProject(sequelize, DataTypes) {
return Project; return Project;
} }
module.exports = defineProject; export default defineProject;

View File

@@ -28,4 +28,4 @@ function defineRunCase(sequelize, DataTypes) {
return RunCase; return RunCase;
} }
module.exports = defineRunCase; export default defineRunCase;

View File

@@ -34,4 +34,4 @@ function defineRun(sequelize, DataTypes) {
return Run; return Run;
} }
module.exports = defineRun; export default defineRun;

View File

@@ -19,4 +19,4 @@ function defineStep(sequelize, DataTypes) {
return Step; return Step;
} }
module.exports = defineStep; export default defineStep;

View File

@@ -1,9 +1,3 @@
/**
*
* @param {import('sequelize').Sequelize} sequelize
* @param {*} DataTypes
* @returns
*/
function defineUser(sequelize, DataTypes) { function defineUser(sequelize, DataTypes) {
const User = sequelize.define( const User = sequelize.define(
'User', 'User',
@@ -39,4 +33,4 @@ function defineUser(sequelize, DataTypes) {
return User; return User;
} }
module.exports = defineUser; export default defineUser;

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,7 @@
"name": "unittcms-backend", "name": "unittcms-backend",
"version": "1.0.0", "version": "1.0.0",
"private": true, "private": true,
"type": "module",
"scripts": { "scripts": {
"start": "node --env-file=.env index", "start": "node --env-file=.env index",
"dev": "node index", "dev": "node index",
@@ -19,8 +20,8 @@
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"papaparse": "^5.5.2", "papaparse": "^5.5.2",
"sequelize": "^6.37.5", "sequelize": "^6.37.7",
"sequelize-cli": "^6.6.2", "sequelize-cli": "^6.6.3",
"sqlite3": "^5.1.7", "sqlite3": "^5.1.7",
"xmlbuilder2": "^3.1.1" "xmlbuilder2": "^3.1.1"
} }

View File

@@ -1,13 +1,17 @@
const fs = require('fs'); import fs from 'fs';
const path = require('path'); import path from 'path';
const express = require('express'); import { fileURLToPath } from 'url';
import express from 'express';
const router = express.Router(); const router = express.Router();
const { DataTypes } = require('sequelize'); import { DataTypes } from 'sequelize';
const defineAttachment = require('../../models/attachments'); import defineAttachment from '../../models/attachments.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
module.exports = function (sequelize) { export default function (sequelize) {
const Attachment = defineAttachment(sequelize, DataTypes); const Attachment = defineAttachment(sequelize, DataTypes);
// TODO middleware to verify user permission to delete attachment
router.delete('/:attachmentId', async (req, res) => { router.delete('/:attachmentId', async (req, res) => {
const attachmentId = req.params.attachmentId; const attachmentId = req.params.attachmentId;
const t = await sequelize.transaction(); const t = await sequelize.transaction();
@@ -41,4 +45,4 @@ module.exports = function (sequelize) {
}); });
return router; return router;
}; }

View File

@@ -1,12 +1,17 @@
const fs = require('fs'); import fs from 'fs';
const path = require('path'); import path from 'path';
const express = require('express'); import { fileURLToPath } from 'url';
import express from 'express';
const router = express.Router(); const router = express.Router();
const { DataTypes } = require('sequelize'); import { DataTypes } from 'sequelize';
const defineAttachment = require('../../models/attachments'); import defineAttachment from '../../models/attachments.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
module.exports = function (sequelize) { export default function (sequelize) {
const Attachment = defineAttachment(sequelize, DataTypes); const Attachment = defineAttachment(sequelize, DataTypes);
// TODO middleware to verify user permission to get attachment
router.get('/download/:attachmentId', async (req, res) => { router.get('/download/:attachmentId', async (req, res) => {
const attachmentId = req.params.attachmentId; const attachmentId = req.params.attachmentId;
@@ -30,4 +35,4 @@ module.exports = function (sequelize) {
}); });
return router; return router;
}; }

View File

@@ -1,13 +1,16 @@
const fs = require('fs'); import fs from 'fs';
const path = require('path'); import path from 'path';
const express = require('express'); import { fileURLToPath } from 'url';
import multer from 'multer';
import express from 'express';
const router = express.Router(); const router = express.Router();
const multer = require('multer'); import { DataTypes } from 'sequelize';
const { DataTypes } = require('sequelize'); import defineAttachment from '../../models/attachments.js';
const defineAttachment = require('../../models/attachments'); import defineCaseAttachment from '../../models/caseAttachments.js';
const defineCaseAttachment = require('../../models/caseAttachments'); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
module.exports = function (sequelize) { export default function (sequelize) {
const Attachment = defineAttachment(sequelize, DataTypes); const Attachment = defineAttachment(sequelize, DataTypes);
const CaseAttachment = defineCaseAttachment(sequelize, DataTypes); const CaseAttachment = defineCaseAttachment(sequelize, DataTypes);
@@ -46,6 +49,7 @@ module.exports = function (sequelize) {
const upload = multer({ storage }); const upload = multer({ storage });
// TODO middleware to verify user permission to upload files
router.post('/', upload.array('files', 10), async (req, res) => { router.post('/', upload.array('files', 10), async (req, res) => {
const t = await sequelize.transaction(); const t = await sequelize.transaction();
try { try {
@@ -79,4 +83,4 @@ module.exports = function (sequelize) {
}); });
return router; return router;
}; }

View File

@@ -1,11 +1,13 @@
const express = require('express'); import express from 'express';
const router = express.Router(); const router = express.Router();
const { DataTypes } = require('sequelize'); import { DataTypes } from 'sequelize';
const defineCase = require('../../models/cases'); import defineCase from '../../models/cases.js';
import authMiddleware from '../../middleware/auth.js';
import editableMiddleware from '../../middleware/verifyEditable.js';
module.exports = function (sequelize) { export default function (sequelize) {
const { verifySignedIn } = require('../../middleware/auth')(sequelize); const { verifySignedIn } = authMiddleware(sequelize);
const { verifyProjectDeveloperFromProjectId } = require('../../middleware/verifyEditable')(sequelize); const { verifyProjectDeveloperFromProjectId } = editableMiddleware(sequelize);
const Case = defineCase(sequelize, DataTypes); const Case = defineCase(sequelize, DataTypes);
router.post('/bulkdelete', verifySignedIn, verifyProjectDeveloperFromProjectId, async (req, res) => { router.post('/bulkdelete', verifySignedIn, verifyProjectDeveloperFromProjectId, async (req, res) => {
@@ -24,4 +26,4 @@ module.exports = function (sequelize) {
}); });
return router; return router;
}; }

View File

@@ -1,13 +1,15 @@
const express = require('express'); import express from 'express';
const router = express.Router(); const router = express.Router();
const { DataTypes } = require('sequelize'); import { DataTypes } from 'sequelize';
const Papa = require('papaparse'); import Papa from 'papaparse';
const defineCase = require('../../models/cases'); import defineCase from '../../models/cases.js';
import authMiddleware from '../../middleware/auth.js';
import visibilityMiddleware from '../../middleware/verifyVisible.js';
module.exports = function (sequelize) { export default function (sequelize) {
const Case = defineCase(sequelize, DataTypes); const Case = defineCase(sequelize, DataTypes);
const { verifySignedIn } = require('../../middleware/auth')(sequelize); const { verifySignedIn } = authMiddleware(sequelize);
const { verifyProjectVisibleFromFolderId } = require('../../middleware/verifyVisible')(sequelize); const { verifyProjectVisibleFromFolderId } = visibilityMiddleware(sequelize);
router.get('/download', verifySignedIn, verifyProjectVisibleFromFolderId, async (req, res) => { router.get('/download', verifySignedIn, verifyProjectVisibleFromFolderId, async (req, res) => {
const { folderId, type } = req.query; const { folderId, type } = req.query;
@@ -51,4 +53,4 @@ module.exports = function (sequelize) {
}); });
return router; return router;
}; }

View File

@@ -1,11 +1,13 @@
const express = require('express'); import express from 'express';
const router = express.Router(); const router = express.Router();
const { DataTypes } = require('sequelize'); import { DataTypes } from 'sequelize';
const defineCase = require('../../models/cases'); import defineCase from '../../models/cases.js';
import authMiddleware from '../../middleware/auth.js';
import editableMiddleware from '../../middleware/verifyEditable.js';
module.exports = function (sequelize) { export default function (sequelize) {
const { verifySignedIn } = require('../../middleware/auth')(sequelize); const { verifySignedIn } = authMiddleware(sequelize);
const { verifyProjectDeveloperFromCaseId } = require('../../middleware/verifyEditable')(sequelize); const { verifyProjectDeveloperFromCaseId } = editableMiddleware(sequelize);
const Case = defineCase(sequelize, DataTypes); const Case = defineCase(sequelize, DataTypes);
router.put('/:caseId', verifySignedIn, verifyProjectDeveloperFromCaseId, async (req, res) => { router.put('/:caseId', verifySignedIn, verifyProjectDeveloperFromCaseId, async (req, res) => {
@@ -30,4 +32,4 @@ module.exports = function (sequelize) {
}); });
return router; return router;
}; }

View File

@@ -1,25 +1,61 @@
const express = require('express'); import express from 'express';
const router = express.Router(); const router = express.Router();
const { DataTypes } = require('sequelize'); import { DataTypes, Op } from 'sequelize';
const defineCase = require('../../models/cases'); import defineCase from '../../models/cases.js';
import authMiddleware from '../../middleware/auth.js';
import visibilityMiddleware from '../../middleware/verifyVisible.js';
module.exports = function (sequelize) { export default function (sequelize) {
const { verifySignedIn } = authMiddleware(sequelize);
const { verifyProjectVisibleFromFolderId } = visibilityMiddleware(sequelize);
const Case = defineCase(sequelize, DataTypes); const Case = defineCase(sequelize, DataTypes);
const { verifySignedIn } = require('../../middleware/auth')(sequelize);
const { verifyProjectVisibleFromFolderId } = require('../../middleware/verifyVisible')(sequelize);
router.get('/', verifySignedIn, verifyProjectVisibleFromFolderId, async (req, res) => { router.get('/', verifySignedIn, verifyProjectVisibleFromFolderId, async (req, res) => {
const { folderId } = req.query; const { folderId, title, priority, type } = req.query;
if (!folderId) { if (!folderId) {
return res.status(400).json({ error: 'folderId is required' }); return res.status(400).json({ error: 'folderId is required' });
} }
try { try {
const cases = await Case.findAll({ const whereClause = {
where: {
folderId: folderId, folderId: folderId,
}, };
if (title) {
const searchTerm = title.trim();
if (searchTerm.length > 100) {
return res.status(400).json({ error: 'too long title param' });
}
if (searchTerm.length >= 1) {
whereClause[Op.or] = [{ title: { [Op.like]: `%${searchTerm}%` } }];
}
}
if (priority) {
const priorityValues = priority
.split(',')
.map((p) => parseInt(p.trim(), 10))
.filter((p) => !isNaN(p));
if (priorityValues.length > 0) {
whereClause.priority = { [Op.in]: priorityValues };
}
}
if (type) {
const typeValues = type
.split(',')
.map((t) => parseInt(t.trim(), 10))
.filter((t) => !isNaN(t));
if (typeValues.length > 0) {
whereClause.type = { [Op.in]: typeValues };
}
}
const cases = await Case.findAll({
where: whereClause,
}); });
res.json(cases); res.json(cases);
} catch (error) { } catch (error) {
@@ -29,4 +65,4 @@ module.exports = function (sequelize) {
}); });
return router; return router;
}; }

View File

@@ -0,0 +1,74 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import request from 'supertest';
import express from 'express';
import { Sequelize, Op } from 'sequelize';
import casesIndexRoute from './index';
// mock of authentication middleware
vi.mock('../../middleware/auth.js', () => ({
default: () => ({
verifySignedIn: vi.fn((req, res, next) => {
req.userId = 1;
next();
}),
}),
}));
vi.mock('../../middleware/verifyVisible.js', () => ({
default: () => ({
verifyProjectVisibleFromFolderId: vi.fn((req, res, next) => {
next();
}),
}),
}));
// mock defineCase
const mockCase = {
findAll: vi.fn(),
};
vi.mock('../../models/cases.js', () => ({
default: () => mockCase,
}));
describe('GET /cases', () => {
let app;
const sequelize = new Sequelize({
dialect: 'sqlite',
logging: false,
});
beforeEach(() => {
app = express();
app.use(express.json());
// Mount the index route
app.use('/cases', casesIndexRoute(sequelize));
});
it('should return 400 if folderId is missing', async () => {
const res = await request(app).get('/cases');
expect(res.status).toBe(400);
expect(res.body.error).toBe('folderId is required');
});
it('should call findAll with correct where clause for folderId only', async () => {
mockCase.findAll.mockResolvedValue([{ id: 1 }]);
const res = await request(app).get('/cases?folderId=1');
expect(res.status).toBe(200);
expect(mockCase.findAll).toHaveBeenCalledWith({ where: { folderId: '1' } });
expect(res.body).toEqual([{ id: 1 }]);
});
it('should build a where clause based on query parameters.', async () => {
mockCase.findAll.mockResolvedValue([{ id: 1 }]);
const res = await request(app).get('/cases?folderId=1&priority=1,2&type=3');
expect(res.status).toBe(200);
expect(mockCase.findAll).toHaveBeenCalledWith({
where: {
folderId: '1',
priority: { [Op.in]: [1, 2] },
type: { [Op.in]: [3] },
},
});
});
});

View File

@@ -1,12 +1,14 @@
const express = require('express'); import express from 'express';
const router = express.Router(); const router = express.Router();
const { DataTypes } = require('sequelize'); import { DataTypes } from 'sequelize';
const defineProject = require('../../models/projects'); import defineProject from '../../models/projects.js';
const defineFolder = require('../../models/folders'); import defineFolder from '../../models/folders.js';
const defineCase = require('../../models/cases'); import defineCase from '../../models/cases.js';
const defineRunCase = require('../../models/runCases'); import defineRunCase from '../../models/runCases.js';
import authMiddleware from '../../middleware/auth.js';
import visibilityMiddleware from '../../middleware/verifyVisible.js';
module.exports = function (sequelize) { export default function (sequelize) {
const Project = defineProject(sequelize, DataTypes); const Project = defineProject(sequelize, DataTypes);
const Folder = defineFolder(sequelize, DataTypes); const Folder = defineFolder(sequelize, DataTypes);
const Case = defineCase(sequelize, DataTypes); const Case = defineCase(sequelize, DataTypes);
@@ -17,8 +19,8 @@ module.exports = function (sequelize) {
Case.belongsTo(Folder, { foreignKey: 'folderId' }); Case.belongsTo(Folder, { foreignKey: 'folderId' });
Case.hasMany(RunCase, { foreignKey: 'caseId' }); Case.hasMany(RunCase, { foreignKey: 'caseId' });
RunCase.belongsTo(Case, { foreignKey: 'caseId' }); RunCase.belongsTo(Case, { foreignKey: 'caseId' });
const { verifySignedIn } = require('../../middleware/auth')(sequelize); const { verifySignedIn } = authMiddleware(sequelize);
const { verifyProjectVisibleFromProjectId } = require('../../middleware/verifyVisible')(sequelize); const { verifyProjectVisibleFromProjectId } = visibilityMiddleware(sequelize);
router.get('/byproject', verifySignedIn, verifyProjectVisibleFromProjectId, async (req, res) => { router.get('/byproject', verifySignedIn, verifyProjectVisibleFromProjectId, async (req, res) => {
const { projectId } = req.query; const { projectId } = req.query;
@@ -51,4 +53,4 @@ module.exports = function (sequelize) {
}); });
return router; return router;
}; }

View File

@@ -1,7 +1,9 @@
const express = require('express'); import express from 'express';
const router = express.Router(); const router = express.Router();
const { DataTypes } = require('sequelize'); import { DataTypes } from 'sequelize';
const defineCase = require('../../models/cases'); import defineCase from '../../models/cases.js';
import authMiddleware from '../../middleware/auth.js';
import editableMiddleware from '../../middleware/verifyEditable.js';
const requiredFields = ['title', 'state', 'priority', 'type', 'automationStatus', 'template']; const requiredFields = ['title', 'state', 'priority', 'type', 'automationStatus', 'template'];
@@ -13,9 +15,9 @@ function isEmpty(value) {
} }
} }
module.exports = function (sequelize) { export default function (sequelize) {
const { verifySignedIn } = require('../../middleware/auth')(sequelize); const { verifySignedIn } = authMiddleware(sequelize);
const { verifyProjectDeveloperFromFolderId } = require('../../middleware/verifyEditable')(sequelize); const { verifyProjectDeveloperFromFolderId } = editableMiddleware(sequelize);
const Case = defineCase(sequelize, DataTypes); const Case = defineCase(sequelize, DataTypes);
router.post('/', verifySignedIn, verifyProjectDeveloperFromFolderId, async (req, res) => { router.post('/', verifySignedIn, verifyProjectDeveloperFromFolderId, async (req, res) => {
@@ -56,4 +58,4 @@ module.exports = function (sequelize) {
}); });
return router; return router;
}; }

View File

@@ -1,11 +1,13 @@
const express = require('express'); import express from 'express';
const router = express.Router(); const router = express.Router();
const { DataTypes } = require('sequelize'); import { DataTypes } from 'sequelize';
const defineCase = require('../../models/cases'); import defineCase from '../../models/cases.js';
const defineStep = require('../../models/steps'); import defineStep from '../../models/steps.js';
const defineAttachment = require('../../models/attachments'); import defineAttachment from '../../models/attachments.js';
import authMiddleware from '../../middleware/auth.js';
import visibilityMiddleware from '../../middleware/verifyVisible.js';
module.exports = function (sequelize) { export default function (sequelize) {
const Case = defineCase(sequelize, DataTypes); const Case = defineCase(sequelize, DataTypes);
const Step = defineStep(sequelize, DataTypes); const Step = defineStep(sequelize, DataTypes);
const Attachment = defineAttachment(sequelize, DataTypes); const Attachment = defineAttachment(sequelize, DataTypes);
@@ -13,8 +15,8 @@ module.exports = function (sequelize) {
Step.belongsToMany(Case, { through: 'caseSteps' }); Step.belongsToMany(Case, { through: 'caseSteps' });
Case.belongsToMany(Attachment, { through: 'caseAttachments' }); Case.belongsToMany(Attachment, { through: 'caseAttachments' });
Attachment.belongsToMany(Case, { through: 'caseAttachments' }); Attachment.belongsToMany(Case, { through: 'caseAttachments' });
const { verifySignedIn } = require('../../middleware/auth')(sequelize); const { verifySignedIn } = authMiddleware(sequelize);
const { verifyProjectVisibleFromCaseId } = require('../../middleware/verifyVisible')(sequelize); const { verifyProjectVisibleFromCaseId } = visibilityMiddleware(sequelize);
router.get('/:caseId', verifySignedIn, verifyProjectVisibleFromCaseId, async (req, res) => { router.get('/:caseId', verifySignedIn, verifyProjectVisibleFromCaseId, async (req, res) => {
const caseId = req.params.caseId; const caseId = req.params.caseId;
@@ -43,4 +45,4 @@ module.exports = function (sequelize) {
}); });
return router; return router;
}; }

View File

@@ -1,11 +1,13 @@
const express = require('express'); import express from 'express';
const router = express.Router(); const router = express.Router();
const { DataTypes } = require('sequelize'); import { DataTypes } from 'sequelize';
const defineFolder = require('../../models/folders'); import defineFolder from '../../models/folders.js';
import authMiddleware from '../../middleware/auth.js';
import editableMiddleware from '../../middleware/verifyEditable.js';
module.exports = function (sequelize) { export default function (sequelize) {
const { verifySignedIn } = require('../../middleware/auth')(sequelize); const { verifySignedIn } = authMiddleware(sequelize);
const { verifyProjectDeveloperFromFolderId } = require('../../middleware/verifyEditable')(sequelize); const { verifyProjectDeveloperFromFolderId } = editableMiddleware(sequelize);
const Folder = defineFolder(sequelize, DataTypes); const Folder = defineFolder(sequelize, DataTypes);
router.delete('/:folderId', verifySignedIn, verifyProjectDeveloperFromFolderId, async (req, res) => { router.delete('/:folderId', verifySignedIn, verifyProjectDeveloperFromFolderId, async (req, res) => {
@@ -24,4 +26,4 @@ module.exports = function (sequelize) {
}); });
return router; return router;
}; }

View File

@@ -1,11 +1,13 @@
const express = require('express'); import express from 'express';
const router = express.Router(); const router = express.Router();
const { DataTypes } = require('sequelize'); import { DataTypes } from 'sequelize';
const defineFolder = require('../../models/folders'); import defineFolder from '../../models/folders.js';
import authMiddleware from '../../middleware/auth.js';
import editableMiddleware from '../../middleware/verifyEditable.js';
module.exports = function (sequelize) { export default function (sequelize) {
const { verifySignedIn } = require('../../middleware/auth')(sequelize); const { verifySignedIn } = authMiddleware(sequelize);
const { verifyProjectDeveloperFromFolderId } = require('../../middleware/verifyEditable')(sequelize); const { verifyProjectDeveloperFromFolderId } = editableMiddleware(sequelize);
const Folder = defineFolder(sequelize, DataTypes); const Folder = defineFolder(sequelize, DataTypes);
router.put('/:folderId', verifySignedIn, verifyProjectDeveloperFromFolderId, async (req, res) => { router.put('/:folderId', verifySignedIn, verifyProjectDeveloperFromFolderId, async (req, res) => {
@@ -30,4 +32,4 @@ module.exports = function (sequelize) {
}); });
return router; return router;
}; }

View File

@@ -1,11 +1,13 @@
const express = require('express'); import express from 'express';
const router = express.Router(); const router = express.Router();
const { DataTypes } = require('sequelize'); import { DataTypes } from 'sequelize';
const defineFolder = require('../../models/folders'); import defineFolder from '../../models/folders.js';
import authMiddleware from '../../middleware/auth.js';
import visibilityMiddleware from '../../middleware/verifyVisible.js';
module.exports = function (sequelize) { export default function (sequelize) {
const { verifySignedIn } = require('../../middleware/auth')(sequelize); const { verifySignedIn } = authMiddleware(sequelize);
const { verifyProjectVisibleFromProjectId } = require('../../middleware/verifyVisible')(sequelize); const { verifyProjectVisibleFromProjectId } = visibilityMiddleware(sequelize);
const Folder = defineFolder(sequelize, DataTypes); const Folder = defineFolder(sequelize, DataTypes);
router.get('/', verifySignedIn, verifyProjectVisibleFromProjectId, async (req, res) => { router.get('/', verifySignedIn, verifyProjectVisibleFromProjectId, async (req, res) => {
@@ -29,4 +31,4 @@ module.exports = function (sequelize) {
}); });
return router; return router;
}; }

View File

@@ -1,11 +1,13 @@
const express = require('express'); import express from 'express';
const router = express.Router(); const router = express.Router();
const { DataTypes } = require('sequelize'); import { DataTypes } from 'sequelize';
const defineFolder = require('../../models/folders'); import defineFolder from '../../models/folders.js';
import authMiddleware from '../../middleware/auth.js';
import editableMiddleware from '../../middleware/verifyEditable.js';
module.exports = function (sequelize) { export default function (sequelize) {
const { verifySignedIn } = require('../../middleware/auth')(sequelize); const { verifySignedIn } = authMiddleware(sequelize);
const { verifyProjectDeveloperFromProjectId } = require('../../middleware/verifyEditable')(sequelize); const { verifyProjectDeveloperFromProjectId } = editableMiddleware(sequelize);
const Folder = defineFolder(sequelize, DataTypes); const Folder = defineFolder(sequelize, DataTypes);
router.post('/', verifySignedIn, verifyProjectDeveloperFromProjectId, async (req, res) => { router.post('/', verifySignedIn, verifyProjectDeveloperFromProjectId, async (req, res) => {
@@ -31,4 +33,4 @@ module.exports = function (sequelize) {
}); });
return router; return router;
}; }

View File

@@ -1,10 +1,10 @@
const express = require('express'); import express from 'express';
const router = express.Router(); const router = express.Router();
module.exports = function () { export default function () {
router.get('/', async (req, res) => { router.get('/', async (req, res) => {
res.json({ status: 'ok' }); res.json({ status: 'ok' });
}); });
return router; return router;
}; }

View File

@@ -1,15 +1,17 @@
const express = require('express'); import express from 'express';
const router = express.Router(); const router = express.Router();
const { DataTypes } = require('sequelize'); import { DataTypes } from 'sequelize';
const defineProject = require('../../models/projects'); import defineProject from '../../models/projects.js';
const defineFolder = require('../../models/folders'); import defineFolder from '../../models/folders.js';
const defineCase = require('../../models/cases'); import defineCase from '../../models/cases.js';
const defineRun = require('../../models/runs'); import defineRun from '../../models/runs.js';
const defineRunCase = require('../../models/runCases'); import defineRunCase from '../../models/runCases.js';
import authMiddleware from '../../middleware/auth.js';
import visibilityMiddleware from '../../middleware/verifyVisible.js';
module.exports = function (sequelize) { export default function (sequelize) {
const { verifySignedIn } = require('../../middleware/auth')(sequelize); const { verifySignedIn } = authMiddleware(sequelize);
const { verifyProjectVisibleFromProjectId } = require('../../middleware/verifyVisible')(sequelize); const { verifyProjectVisibleFromProjectId } = visibilityMiddleware(sequelize);
const Project = defineProject(sequelize, DataTypes); const Project = defineProject(sequelize, DataTypes);
const Folder = defineFolder(sequelize, DataTypes); const Folder = defineFolder(sequelize, DataTypes);
@@ -49,4 +51,4 @@ module.exports = function (sequelize) {
}); });
return router; return router;
}; }

View File

@@ -1,9 +1,10 @@
const express = require('express'); import express from 'express';
const router = express.Router(); const router = express.Router();
// "/" GET export default function () {
router.get('/', (req, res) => { router.get('/', (req, res) => {
res.send('This is UnitTCMS API server'); res.send('This is UnitTCMS API server');
}); });
module.exports = router; return router;
}

View File

@@ -1,11 +1,12 @@
const express = require('express'); import express from 'express';
const router = express.Router(); const router = express.Router();
const { DataTypes } = require('sequelize'); import { DataTypes } from 'sequelize';
const defineMember = require('../../models/members'); import defineMember from '../../models/members.js';
const defineProject = require('../../models/projects'); import defineProject from '../../models/projects.js';
import authMiddleware from '../../middleware/auth.js';
module.exports = function (sequelize) { export default function (sequelize) {
const { verifySignedIn } = require('../../middleware/auth')(sequelize); const { verifySignedIn } = authMiddleware(sequelize);
const Member = defineMember(sequelize, DataTypes); const Member = defineMember(sequelize, DataTypes);
const Project = defineProject(sequelize, DataTypes); const Project = defineProject(sequelize, DataTypes);
@@ -45,4 +46,4 @@ module.exports = function (sequelize) {
}); });
return router; return router;
}; }

View File

@@ -1,11 +1,13 @@
const express = require('express'); import express from 'express';
const router = express.Router(); const router = express.Router();
const { DataTypes } = require('sequelize'); import { DataTypes } from 'sequelize';
const defineMember = require('../../models/members'); import defineMember from '../../models/members.js';
import authMiddleware from '../../middleware/auth.js';
import editableMiddleware from '../../middleware/verifyEditable.js';
module.exports = function (sequelize) { export default function (sequelize) {
const { verifySignedIn } = require('../../middleware/auth')(sequelize); const { verifySignedIn } = authMiddleware(sequelize);
const { verifyProjectManagerFromProjectId } = require('../../middleware/verifyEditable')(sequelize); const { verifyProjectManagerFromProjectId } = editableMiddleware(sequelize);
const Member = defineMember(sequelize, DataTypes); const Member = defineMember(sequelize, DataTypes);
router.delete('/', verifySignedIn, verifyProjectManagerFromProjectId, async (req, res) => { router.delete('/', verifySignedIn, verifyProjectManagerFromProjectId, async (req, res) => {
@@ -34,4 +36,4 @@ module.exports = function (sequelize) {
}); });
return router; return router;
}; }

View File

@@ -1,11 +1,13 @@
const express = require('express'); import express from 'express';
const router = express.Router(); const router = express.Router();
const { DataTypes } = require('sequelize'); import { DataTypes } from 'sequelize';
const defineMember = require('../../models/members'); import defineMember from '../../models/members.js';
import authMiddleware from '../../middleware/auth.js';
import editableMiddleware from '../../middleware/verifyEditable.js';
module.exports = function (sequelize) { export default function (sequelize) {
const { verifySignedIn } = require('../../middleware/auth')(sequelize); const { verifySignedIn } = authMiddleware(sequelize);
const { verifyProjectManagerFromProjectId } = require('../../middleware/verifyEditable')(sequelize); const { verifyProjectManagerFromProjectId } = editableMiddleware(sequelize);
const Member = defineMember(sequelize, DataTypes); const Member = defineMember(sequelize, DataTypes);
router.put('/', verifySignedIn, verifyProjectManagerFromProjectId, async (req, res) => { router.put('/', verifySignedIn, verifyProjectManagerFromProjectId, async (req, res) => {
@@ -38,4 +40,4 @@ module.exports = function (sequelize) {
}); });
return router; return router;
}; }

View File

@@ -1,12 +1,14 @@
const express = require('express'); import express from 'express';
const router = express.Router(); const router = express.Router();
const { DataTypes } = require('sequelize'); import { DataTypes } from 'sequelize';
const defineUser = require('../../models/users'); import defineUser from '../../models/users.js';
const defineMember = require('../../models/members'); import defineMember from '../../models/members.js';
import authMiddleware from '../../middleware/auth.js';
import visibilityMiddleware from '../../middleware/verifyVisible.js';
module.exports = function (sequelize) { export default function (sequelize) {
const { verifySignedIn } = require('../../middleware/auth')(sequelize); const { verifySignedIn } = authMiddleware(sequelize);
const { verifyProjectVisibleFromProjectId } = require('../../middleware/verifyVisible')(sequelize); const { verifyProjectVisibleFromProjectId } = visibilityMiddleware(sequelize);
const User = defineUser(sequelize, DataTypes); const User = defineUser(sequelize, DataTypes);
const Member = defineMember(sequelize, DataTypes); const Member = defineMember(sequelize, DataTypes);
Member.belongsTo(User, { foreignKey: 'userId' }); Member.belongsTo(User, { foreignKey: 'userId' });
@@ -37,4 +39,4 @@ module.exports = function (sequelize) {
}); });
return router; return router;
}; }

View File

@@ -1,12 +1,14 @@
const express = require('express'); import express from 'express';
const router = express.Router(); const router = express.Router();
const { DataTypes } = require('sequelize'); import { DataTypes } from 'sequelize';
const defineMember = require('../../models/members'); import defineMember from '../../models/members.js';
const { memberRoles } = require('../../routes/users/authSettings'); import { memberRoles } from '../../routes/users/authSettings.js';
import authMiddleware from '../../middleware/auth.js';
import editableMiddleware from '../../middleware/verifyEditable.js';
module.exports = function (sequelize) { export default function (sequelize) {
const { verifySignedIn } = require('../../middleware/auth')(sequelize); const { verifySignedIn } = authMiddleware(sequelize);
const { verifyProjectManagerFromProjectId } = require('../../middleware/verifyEditable')(sequelize); const { verifyProjectManagerFromProjectId } = editableMiddleware(sequelize);
const Member = defineMember(sequelize, DataTypes); const Member = defineMember(sequelize, DataTypes);
router.post('/', verifySignedIn, verifyProjectManagerFromProjectId, async (req, res) => { router.post('/', verifySignedIn, verifyProjectManagerFromProjectId, async (req, res) => {
@@ -41,4 +43,4 @@ module.exports = function (sequelize) {
}); });
return router; return router;
}; }

View File

@@ -1,13 +1,15 @@
const express = require('express'); import express from 'express';
const router = express.Router(); const router = express.Router();
const { DataTypes } = require('sequelize'); import { DataTypes } from 'sequelize';
const defineProject = require('../../models/projects'); import defineProject from '../../models/projects.js';
const defineFolder = require('../../models/folders'); import defineFolder from '../../models/folders.js';
const defineRun = require('../../models/runs'); import defineRun from '../../models/runs.js';
import authMiddleware from '../../middleware/auth.js';
import editableMiddleware from '../../middleware/verifyEditable.js';
module.exports = function (sequelize) { export default function (sequelize) {
const { verifySignedIn } = require('../../middleware/auth')(sequelize); const { verifySignedIn } = authMiddleware(sequelize);
const { verifyProjectOwner } = require('../../middleware/verifyEditable')(sequelize); const { verifyProjectOwner } = editableMiddleware(sequelize);
const Project = defineProject(sequelize, DataTypes); const Project = defineProject(sequelize, DataTypes);
const Folder = defineFolder(sequelize, DataTypes); const Folder = defineFolder(sequelize, DataTypes);
const Run = defineRun(sequelize, DataTypes); const Run = defineRun(sequelize, DataTypes);
@@ -38,4 +40,4 @@ module.exports = function (sequelize) {
}); });
return router; return router;
}; }

View File

@@ -1,11 +1,13 @@
const express = require('express'); import express from 'express';
const router = express.Router(); const router = express.Router();
const { DataTypes } = require('sequelize'); import { DataTypes } from 'sequelize';
const defineProject = require('../../models/projects'); import defineProject from '../../models/projects.js';
import authMiddleware from '../../middleware/auth.js';
import editableMiddleware from '../../middleware/verifyEditable.js';
module.exports = function (sequelize) { export default function (sequelize) {
const { verifySignedIn } = require('../../middleware/auth')(sequelize); const { verifySignedIn } = authMiddleware(sequelize);
const { verifyProjectOwner } = require('../../middleware/verifyEditable')(sequelize); const { verifyProjectOwner } = editableMiddleware(sequelize);
const Project = defineProject(sequelize, DataTypes); const Project = defineProject(sequelize, DataTypes);
router.put('/:projectId', verifySignedIn, verifyProjectOwner, async (req, res) => { router.put('/:projectId', verifySignedIn, verifyProjectOwner, async (req, res) => {
@@ -29,4 +31,4 @@ module.exports = function (sequelize) {
}); });
return router; return router;
}; }

View File

@@ -1,11 +1,12 @@
const express = require('express'); import express from 'express';
const router = express.Router(); const router = express.Router();
const { DataTypes, Op } = require('sequelize'); import { DataTypes, Op } from 'sequelize';
const defineProject = require('../../models/projects'); import defineProject from '../../models/projects.js';
const defineMember = require('../../models/members'); import defineMember from '../../models/members.js';
import authMiddleware from '../../middleware/auth.js';
module.exports = function (sequelize) { export default function (sequelize) {
const { verifySignedIn } = require('../../middleware/auth')(sequelize); const { verifySignedIn } = authMiddleware(sequelize);
const Project = defineProject(sequelize, DataTypes); const Project = defineProject(sequelize, DataTypes);
const Member = defineMember(sequelize, DataTypes); const Member = defineMember(sequelize, DataTypes);
Project.hasMany(Member, { foreignKey: 'projectId' }); Project.hasMany(Member, { foreignKey: 'projectId' });
@@ -49,4 +50,4 @@ module.exports = function (sequelize) {
}); });
return router; return router;
}; }

View File

@@ -1,10 +1,11 @@
const express = require('express'); import express from 'express';
const router = express.Router(); const router = express.Router();
const { DataTypes } = require('sequelize'); import { DataTypes } from 'sequelize';
const defineProject = require('../../models/projects'); import defineProject from '../../models/projects.js';
import authMiddleware from '../../middleware/auth.js';
module.exports = function (sequelize) { export default function (sequelize) {
const { verifySignedIn } = require('../../middleware/auth')(sequelize); const { verifySignedIn } = authMiddleware(sequelize);
const Project = defineProject(sequelize, DataTypes); const Project = defineProject(sequelize, DataTypes);
router.post('/', verifySignedIn, async (req, res) => { router.post('/', verifySignedIn, async (req, res) => {
@@ -24,4 +25,4 @@ module.exports = function (sequelize) {
}); });
return router; return router;
}; }

View File

@@ -1,12 +1,14 @@
const express = require('express'); import express from 'express';
const router = express.Router(); const router = express.Router();
const { DataTypes } = require('sequelize'); import { DataTypes } from 'sequelize';
const defineProject = require('../../models/projects'); import defineProject from '../../models/projects.js';
const defineFolder = require('../../models/folders'); import defineFolder from '../../models/folders.js';
import authMiddleware from '../../middleware/auth.js';
import visibilityMiddleware from '../../middleware/verifyVisible.js';
module.exports = function (sequelize) { export default function (sequelize) {
const { verifySignedIn } = require('../../middleware/auth')(sequelize); const { verifySignedIn } = authMiddleware(sequelize);
const { verifyProjectVisibleFromProjectId } = require('../../middleware/verifyVisible')(sequelize); const { verifyProjectVisibleFromProjectId } = visibilityMiddleware(sequelize);
const Project = defineProject(sequelize, DataTypes); const Project = defineProject(sequelize, DataTypes);
const Folder = defineFolder(sequelize, DataTypes); const Folder = defineFolder(sequelize, DataTypes);
Project.hasMany(Folder, { foreignKey: 'projectId' }); Project.hasMany(Folder, { foreignKey: 'projectId' });
@@ -36,4 +38,4 @@ module.exports = function (sequelize) {
}); });
return router; return router;
}; }

View File

@@ -1,11 +1,13 @@
const express = require('express'); import express from 'express';
const router = express.Router(); const router = express.Router();
const { DataTypes } = require('sequelize'); import { DataTypes } from 'sequelize';
const defineRunCase = require('../../models/runCases'); import defineRunCase from '../../models/runCases.js';
import authMiddleware from '../../middleware/auth.js';
import editableMiddleware from '../../middleware/verifyEditable.js';
module.exports = function (sequelize) { export default function (sequelize) {
const { verifySignedIn } = require('../../middleware/auth')(sequelize); const { verifySignedIn } = authMiddleware(sequelize);
const { verifyProjectReporterFromRunId } = require('../../middleware/verifyEditable')(sequelize); const { verifyProjectReporterFromRunId } = editableMiddleware(sequelize);
const RunCase = defineRunCase(sequelize, DataTypes); const RunCase = defineRunCase(sequelize, DataTypes);
router.post('/update', verifySignedIn, verifyProjectReporterFromRunId, async (req, res) => { router.post('/update', verifySignedIn, verifyProjectReporterFromRunId, async (req, res) => {
@@ -71,4 +73,4 @@ module.exports = function (sequelize) {
}); });
return router; return router;
}; }

View File

@@ -1,11 +1,13 @@
const express = require('express'); import express from 'express';
const router = express.Router(); const router = express.Router();
const { DataTypes } = require('sequelize'); import { DataTypes } from 'sequelize';
const defineRunCase = require('../../models/runCases'); import defineRunCase from '../../models/runCases.js';
import authMiddleware from '../../middleware/auth.js';
import visibilityMiddleware from '../../middleware/verifyVisible.js';
module.exports = function (sequelize) { export default function (sequelize) {
const { verifySignedIn } = require('../../middleware/auth')(sequelize); const { verifySignedIn } = authMiddleware(sequelize);
const { verifyProjectVisibleFromRunId } = require('../../middleware/verifyVisible')(sequelize); const { verifyProjectVisibleFromRunId } = visibilityMiddleware(sequelize);
const RunCase = defineRunCase(sequelize, DataTypes); const RunCase = defineRunCase(sequelize, DataTypes);
router.get('/', verifySignedIn, verifyProjectVisibleFromRunId, async (req, res) => { router.get('/', verifySignedIn, verifyProjectVisibleFromRunId, async (req, res) => {
@@ -29,4 +31,4 @@ module.exports = function (sequelize) {
}); });
return router; return router;
}; }

View File

@@ -1,11 +1,13 @@
const express = require('express'); import express from 'express';
const router = express.Router(); const router = express.Router();
const { DataTypes } = require('sequelize'); import { DataTypes } from 'sequelize';
const defineRun = require('../../models/runs'); import defineRun from '../../models/runs.js';
import authMiddleware from '../../middleware/auth.js';
import editableMiddleware from '../../middleware/verifyEditable.js';
module.exports = function (sequelize) { export default function (sequelize) {
const { verifySignedIn } = require('../../middleware/auth')(sequelize); const { verifySignedIn } = authMiddleware(sequelize);
const { verifyProjectReporterFromRunId } = require('../../middleware/verifyEditable')(sequelize); const { verifyProjectReporterFromRunId } = editableMiddleware(sequelize);
const Run = defineRun(sequelize, DataTypes); const Run = defineRun(sequelize, DataTypes);
router.delete('/:runId', verifySignedIn, verifyProjectReporterFromRunId, async (req, res) => { router.delete('/:runId', verifySignedIn, verifyProjectReporterFromRunId, async (req, res) => {
@@ -24,4 +26,4 @@ module.exports = function (sequelize) {
}); });
return router; return router;
}; }

View File

@@ -1,17 +1,18 @@
const express = require('express'); import express from 'express';
const router = express.Router(); const router = express.Router();
const Papa = require('papaparse'); import { DataTypes } from 'sequelize';
const { create } = require('xmlbuilder2'); import Papa from 'papaparse';
import { create } from 'xmlbuilder2';
import defineRun from '../../models/runs.js';
import defineRunCase from '../../models/runCases.js';
import defineCase from '../../models/cases.js';
import defineFolder from '../../models/folders.js';
import authMiddleware from '../../middleware/auth.js';
import visibilityMiddleware from '../../middleware/verifyVisible.js';
const defineRun = require('../../models/runs'); export default function (sequelize) {
const defineRunCase = require('../../models/runCases'); const { verifySignedIn } = authMiddleware(sequelize);
const defineCase = require('../../models/cases'); const { verifyProjectVisibleFromRunId } = visibilityMiddleware(sequelize);
const defineFolder = require('../../models/folders');
module.exports = function (sequelize) {
const { DataTypes } = require('sequelize');
const { verifySignedIn } = require('../../middleware/auth')(sequelize);
const { verifyProjectVisibleFromRunId } = require('../../middleware/verifyVisible')(sequelize);
const Run = defineRun(sequelize, DataTypes); const Run = defineRun(sequelize, DataTypes);
const RunCase = defineRunCase(sequelize, DataTypes); const RunCase = defineRunCase(sequelize, DataTypes);
@@ -123,4 +124,4 @@ module.exports = function (sequelize) {
}); });
return router; return router;
}; }

View File

@@ -1,12 +1,14 @@
const express = require('express'); import express from 'express';
const router = express.Router(); const router = express.Router();
const { DataTypes } = require('sequelize'); import { DataTypes } from 'sequelize';
const defineRun = require('../../models/runs'); import defineRun from '../../models/runs.js';
import authMiddleware from '../../middleware/auth.js';
import editableMiddleware from '../../middleware/verifyEditable.js';
module.exports = function (sequelize) { export default function (sequelize) {
const Run = defineRun(sequelize, DataTypes); const Run = defineRun(sequelize, DataTypes);
const { verifySignedIn } = require('../../middleware/auth')(sequelize); const { verifySignedIn } = authMiddleware(sequelize);
const { verifyProjectReporterFromRunId } = require('../../middleware/verifyEditable')(sequelize); const { verifyProjectReporterFromRunId } = editableMiddleware(sequelize);
router.put('/:runId', verifySignedIn, verifyProjectReporterFromRunId, async (req, res) => { router.put('/:runId', verifySignedIn, verifyProjectReporterFromRunId, async (req, res) => {
const runId = req.params.runId; const runId = req.params.runId;
@@ -27,4 +29,4 @@ module.exports = function (sequelize) {
}); });
return router; return router;
}; }

View File

@@ -1,11 +1,13 @@
const express = require('express'); import express from 'express';
const router = express.Router(); const router = express.Router();
const { DataTypes } = require('sequelize'); import { DataTypes } from 'sequelize';
const defineRun = require('../../models/runs'); import defineRun from '../../models/runs.js';
import authMiddleware from '../../middleware/auth.js';
import visibilityMiddleware from '../../middleware/verifyVisible.js';
module.exports = function (sequelize) { export default function (sequelize) {
const { verifySignedIn } = require('../../middleware/auth')(sequelize); const { verifySignedIn } = authMiddleware(sequelize);
const { verifyProjectVisibleFromProjectId } = require('../../middleware/verifyVisible')(sequelize); const { verifyProjectVisibleFromProjectId } = visibilityMiddleware(sequelize);
const Run = defineRun(sequelize, DataTypes); const Run = defineRun(sequelize, DataTypes);
router.get('/', verifySignedIn, verifyProjectVisibleFromProjectId, async (req, res) => { router.get('/', verifySignedIn, verifyProjectVisibleFromProjectId, async (req, res) => {
@@ -29,4 +31,4 @@ module.exports = function (sequelize) {
}); });
return router; return router;
}; }

View File

@@ -1,11 +1,13 @@
const express = require('express'); import express from 'express';
const router = express.Router(); const router = express.Router();
const { DataTypes } = require('sequelize'); import { DataTypes } from 'sequelize';
const defineRun = require('../../models/runs'); import defineRun from '../../models/runs.js';
import authMiddleware from '../../middleware/auth.js';
import editableMiddleware from '../../middleware/verifyEditable.js';
module.exports = function (sequelize) { export default function (sequelize) {
const { verifySignedIn } = require('../../middleware/auth')(sequelize); const { verifySignedIn } = authMiddleware(sequelize);
const { verifyProjectReporterFromProjectId } = require('../../middleware/verifyEditable')(sequelize); const { verifyProjectReporterFromProjectId } = editableMiddleware(sequelize);
const Run = defineRun(sequelize, DataTypes); const Run = defineRun(sequelize, DataTypes);
router.post('/', verifySignedIn, verifyProjectReporterFromProjectId, async (req, res) => { router.post('/', verifySignedIn, verifyProjectReporterFromProjectId, async (req, res) => {
@@ -32,4 +34,4 @@ module.exports = function (sequelize) {
}); });
return router; return router;
}; }

View File

@@ -1,12 +1,14 @@
const express = require('express'); import express from 'express';
const router = express.Router(); const router = express.Router();
const { DataTypes, literal } = require('sequelize'); import { DataTypes, literal } from 'sequelize';
const defineRun = require('../../models/runs'); import defineRun from '../../models/runs.js';
const defineRunCase = require('../../models/runCases'); import defineRunCase from '../../models/runCases.js';
import authMiddleware from '../../middleware/auth.js';
import visibilityMiddleware from '../../middleware/verifyVisible.js';
module.exports = function (sequelize) { export default function (sequelize) {
const { verifySignedIn } = require('../../middleware/auth')(sequelize); const { verifySignedIn } = authMiddleware(sequelize);
const { verifyProjectVisibleFromRunId } = require('../../middleware/verifyVisible')(sequelize); const { verifyProjectVisibleFromRunId } = visibilityMiddleware(sequelize);
const Run = defineRun(sequelize, DataTypes); const Run = defineRun(sequelize, DataTypes);
const RunCase = defineRunCase(sequelize, DataTypes); const RunCase = defineRunCase(sequelize, DataTypes);
@@ -40,4 +42,4 @@ module.exports = function (sequelize) {
}); });
return router; return router;
}; }

View File

@@ -1,14 +1,16 @@
const express = require('express'); import express from 'express';
const router = express.Router(); const router = express.Router();
const { DataTypes } = require('sequelize'); import { DataTypes } from 'sequelize';
const defineStep = require('../../models/steps'); import defineStep from '../../models/steps.js';
const defineCaseStep = require('../../models/caseSteps'); import defineCaseStep from '../../models/caseSteps.js';
import authMiddleware from '../../middleware/auth.js';
import editableMiddleware from '../../middleware/verifyEditable.js';
module.exports = function (sequelize) { export default function (sequelize) {
const Step = defineStep(sequelize, DataTypes); const Step = defineStep(sequelize, DataTypes);
const CaseStep = defineCaseStep(sequelize, DataTypes); const CaseStep = defineCaseStep(sequelize, DataTypes);
const { verifySignedIn } = require('../../middleware/auth')(sequelize); const { verifySignedIn } = authMiddleware(sequelize);
const { verifyProjectDeveloperFromCaseId } = require('../../middleware/verifyEditable')(sequelize); const { verifyProjectDeveloperFromCaseId } = editableMiddleware(sequelize);
router.post('/update', verifySignedIn, verifyProjectDeveloperFromCaseId, async (req, res) => { router.post('/update', verifySignedIn, verifyProjectDeveloperFromCaseId, async (req, res) => {
const caseId = req.query.caseId; const caseId = req.query.caseId;
@@ -87,4 +89,4 @@ module.exports = function (sequelize) {
}); });
return router; return router;
}; }

View File

@@ -2,4 +2,4 @@ const roles = [{ uid: 'administrator' }, { uid: 'user' }];
const memberRoles = [{ uid: 'manager' }, { uid: 'developer' }, { uid: 'reporter' }]; const memberRoles = [{ uid: 'manager' }, { uid: 'developer' }, { uid: 'reporter' }];
const defaultDangerKey = 'test-plat-default-key'; const defaultDangerKey = 'test-plat-default-key';
module.exports = { roles, memberRoles, defaultDangerKey }; export { roles, memberRoles, defaultDangerKey };

View File

@@ -1,10 +1,11 @@
const express = require('express'); import express from 'express';
const router = express.Router(); const router = express.Router();
const { DataTypes } = require('sequelize'); import { DataTypes } from 'sequelize';
const defineUser = require('../../models/users'); import defineUser from '../../models/users.js';
import authMiddleware from '../../middleware/auth.js';
module.exports = function (sequelize) { export default function (sequelize) {
const { verifySignedIn } = require('../../middleware/auth')(sequelize); const { verifySignedIn } = authMiddleware(sequelize);
const User = defineUser(sequelize, DataTypes); const User = defineUser(sequelize, DataTypes);
router.get('/find/:userId', verifySignedIn, async (req, res) => { router.get('/find/:userId', verifySignedIn, async (req, res) => {
@@ -33,4 +34,4 @@ module.exports = function (sequelize) {
}); });
return router; return router;
}; }

View File

@@ -1,10 +1,11 @@
const express = require('express'); import express from 'express';
const router = express.Router(); const router = express.Router();
const { DataTypes } = require('sequelize'); import { DataTypes } from 'sequelize';
const defineUser = require('../../models/users'); import defineUser from '../../models/users.js';
import authMiddleware from '../../middleware/auth.js';
module.exports = function (sequelize) { export default function (sequelize) {
const { verifySignedIn, verifyAdmin } = require('../../middleware/auth')(sequelize); const { verifySignedIn, verifyAdmin } = authMiddleware(sequelize);
const User = defineUser(sequelize, DataTypes); const User = defineUser(sequelize, DataTypes);
router.get('/', verifySignedIn, verifyAdmin, async (req, res) => { router.get('/', verifySignedIn, verifyAdmin, async (req, res) => {
@@ -20,4 +21,4 @@ module.exports = function (sequelize) {
}); });
return router; return router;
}; }

View File

@@ -1,11 +1,12 @@
const express = require('express'); import express from 'express';
const router = express.Router(); const router = express.Router();
const { DataTypes, Op } = require('sequelize'); import { DataTypes, Op } from 'sequelize';
const defineUser = require('../../models/users'); import defineUser from '../../models/users.js';
const defineMember = require('../../models/members'); import defineMember from '../../models/members.js';
import authMiddleware from '../../middleware/auth.js';
module.exports = function (sequelize) { export default function (sequelize) {
const { verifySignedIn } = require('../../middleware/auth')(sequelize); const { verifySignedIn } = authMiddleware(sequelize);
const User = defineUser(sequelize, DataTypes); const User = defineUser(sequelize, DataTypes);
const Member = defineMember(sequelize, DataTypes); const Member = defineMember(sequelize, DataTypes);
@@ -42,4 +43,4 @@ module.exports = function (sequelize) {
}); });
return router; return router;
}; }

View File

@@ -1,12 +1,12 @@
const express = require('express'); import express from 'express';
const router = express.Router(); const router = express.Router();
const { DataTypes } = require('sequelize'); import { DataTypes } from 'sequelize';
const bcrypt = require('bcrypt'); import bcrypt from 'bcrypt';
const jwt = require('jsonwebtoken'); import jwt from 'jsonwebtoken';
const defineUser = require('../../models/users'); import defineUser from '../../models/users.js';
const { defaultDangerKey } = require('./authSettings'); import { defaultDangerKey } from './authSettings.js';
module.exports = function (sequelize) { export default function (sequelize) {
const User = defineUser(sequelize, DataTypes); const User = defineUser(sequelize, DataTypes);
const secretKey = process.env.SECRET_KEY || defaultDangerKey; const secretKey = process.env.SECRET_KEY || defaultDangerKey;
@@ -39,4 +39,4 @@ module.exports = function (sequelize) {
}); });
return router; return router;
}; }

View File

@@ -1,12 +1,12 @@
const express = require('express'); import express from 'express';
const router = express.Router(); const router = express.Router();
const { DataTypes } = require('sequelize'); import { DataTypes } from 'sequelize';
const bcrypt = require('bcrypt'); import bcrypt from 'bcrypt';
const jwt = require('jsonwebtoken'); import jwt from 'jsonwebtoken';
const defineUser = require('../../models/users'); import defineUser from '../../models/users.js';
const { roles, defaultDangerKey } = require('./authSettings'); import { roles, defaultDangerKey } from './authSettings.js';
module.exports = function (sequelize) { export default function (sequelize) {
const User = defineUser(sequelize, DataTypes); const User = defineUser(sequelize, DataTypes);
const secretKey = process.env.SECRET_KEY || defaultDangerKey; const secretKey = process.env.SECRET_KEY || defaultDangerKey;
@@ -42,4 +42,4 @@ module.exports = function (sequelize) {
}); });
return router; return router;
}; }

View File

@@ -3,13 +3,13 @@ import request from 'supertest';
import express from 'express'; import express from 'express';
import { Sequelize } from 'sequelize'; import { Sequelize } from 'sequelize';
import updateRoute from './update'; import updateRoute from './update';
import { roles } from './authSettings'; import { roles } from './authSettings.js';
const adminRoleIndex = roles.findIndex((entry) => entry.uid === 'administrator'); const adminRoleIndex = roles.findIndex((entry) => entry.uid === 'administrator');
const userRoleIndex = roles.findIndex((entry) => entry.uid === 'user'); const userRoleIndex = roles.findIndex((entry) => entry.uid === 'user');
// mock of authentication middleware // mock of authentication middleware
vi.mock('../../middleware/auth', () => ({ vi.mock('../../middleware/auth.js', () => ({
default: () => ({ default: () => ({
verifySignedIn: vi.fn((req, res, next) => { verifySignedIn: vi.fn((req, res, next) => {
req.userId = 1; // Mock user ID req.userId = 1; // Mock user ID
@@ -27,7 +27,7 @@ const mockUser = {
count: vi.fn(), count: vi.fn(),
update: vi.fn(), update: vi.fn(),
}; };
vi.mock('../../models/users', () => ({ vi.mock('../../models/users.js', () => ({
default: () => mockUser, default: () => mockUser,
})); }));

View File

@@ -1,9 +1,7 @@
'use strict'; import fs from 'fs';
const fs = require('fs'); import bcrypt from 'bcrypt';
const bcrypt = require('bcrypt');
module.exports = { export async function up(queryInterface) {
up: async (queryInterface) => {
const hashedPassword = await bcrypt.hash('password', 10); const hashedPassword = await bcrypt.hash('password', 10);
// Add projects table records // Add projects table records
@@ -575,15 +573,11 @@ module.exports = {
if (!fs.existsSync(uploadsFolderPath)) { if (!fs.existsSync(uploadsFolderPath)) {
fs.mkdirSync(uploadsFolderPath, { recursive: true }); fs.mkdirSync(uploadsFolderPath, { recursive: true });
} }
fs.copyFile( fs.copyFile(`${sampleFolderPath}/${SeleniumLogoFileName}`, `${uploadsFolderPath}/${SeleniumLogoFileName}`, (err) => {
`${sampleFolderPath}/${SeleniumLogoFileName}`,
`${uploadsFolderPath}/${SeleniumLogoFileName}`,
(err) => {
if (err) { if (err) {
console.log(err); console.log(err);
} }
} });
);
fs.copyFile(`${sampleFolderPath}/${vitestLogoFileName}`, `${uploadsFolderPath}/${vitestLogoFileName}`, (err) => { fs.copyFile(`${sampleFolderPath}/${vitestLogoFileName}`, `${uploadsFolderPath}/${vitestLogoFileName}`, (err) => {
if (err) { if (err) {
console.log(err); console.log(err);
@@ -694,9 +688,8 @@ module.exports = {
updatedAt: new Date(), updatedAt: new Date(),
}, },
]); ]);
}, }
down: async () => { export async function down() {
// do nothing // do nothing
}, }
};

View File

@@ -1,11 +1,12 @@
const path = require('path'); import path from 'path';
const express = require('express'); import express from 'express';
const RateLimit = require('express-rate-limit'); import cors from 'cors';
const { Sequelize } = require('sequelize'); import RateLimit from 'express-rate-limit';
import { Sequelize } from 'sequelize';
const __dirname = path.dirname(new URL(import.meta.url).pathname);
const app = express(); const app = express();
// enable frontend access // enable frontend access
const cors = require('cors');
const frontendOrigin = process.env.FRONTEND_ORIGIN || 'http://localhost:8000'; const frontendOrigin = process.env.FRONTEND_ORIGIN || 'http://localhost:8000';
const corsOptions = { const corsOptions = {
origin: frontendOrigin, origin: frontendOrigin,
@@ -36,116 +37,113 @@ const sequelize = new Sequelize({
}); });
// "/" // "/"
const indexRoute = require('./routes/index'); import indexRoute from './routes/index.js';
app.use('/', indexRoute); app.use('/', indexRoute());
// "/health" // "/health"
const healthIndexRoute = require('./routes/health/index')(); import healthIndexRoute from './routes/health/index.js';
app.use('/health', healthIndexRoute); app.use('/health', healthIndexRoute());
// "users" // "users"
const usersIndexRoute = require('./routes/users/index')(sequelize); import usersIndexRoute from './routes/users/index.js';
const usersFindRoute = require('./routes/users/find')(sequelize); import usersFindRoute from './routes/users/find.js';
const usersSearchRoute = require('./routes/users/search')(sequelize); import usersSearchRoute from './routes/users/search.js';
const signUpRoute = require('./routes/users/signup')(sequelize); import usersUpdateRoute from './routes/users/update.js';
const signInRoute = require('./routes/users/signin')(sequelize); import signUpRoute from './routes/users/signup.js';
app.use('/users', usersIndexRoute); import signInRoute from './routes/users/signin.js';
app.use('/users', usersFindRoute); app.use('/users', usersIndexRoute(sequelize));
app.use('/users', usersSearchRoute); app.use('/users', usersFindRoute(sequelize));
app.use('/users', signUpRoute); app.use('/users', usersSearchRoute(sequelize));
app.use('/users', signInRoute); app.use('/users', usersUpdateRoute(sequelize));
// ESM import app.use('/users', signUpRoute(sequelize));
(async () => { app.use('/users', signInRoute(sequelize));
const updateRoute = await import('./routes/users/update.mjs');
app.use('/users', updateRoute.default(sequelize));
})();
// "/projects" // "/projects"
const projectsIndexRoute = require('./routes/projects/index')(sequelize); import projectsIndexRoute from './routes/projects/index.js';
const projectsShowRoute = require('./routes/projects/show')(sequelize); import projectsShowRoute from './routes/projects/show.js';
const projectsNewRoute = require('./routes/projects/new')(sequelize); import projectsNewRoute from './routes/projects/new.js';
const projectsEditRoute = require('./routes/projects/edit')(sequelize); import projectsEditRoute from './routes/projects/edit.js';
const projectsDeleteRoute = require('./routes/projects/delete')(sequelize); import projectsDeleteRoute from './routes/projects/delete.js';
app.use('/projects', projectsIndexRoute); app.use('/projects', projectsIndexRoute(sequelize));
app.use('/projects', projectsShowRoute); app.use('/projects', projectsShowRoute(sequelize));
app.use('/projects', projectsNewRoute); app.use('/projects', projectsNewRoute(sequelize));
app.use('/projects', projectsEditRoute); app.use('/projects', projectsEditRoute(sequelize));
app.use('/projects', projectsDeleteRoute); app.use('/projects', projectsDeleteRoute(sequelize));
// "/folders" // "/folders"
const foldersIndexRoute = require('./routes/folders/index')(sequelize); import foldersIndexRoute from './routes/folders/index.js';
const foldersNewRoute = require('./routes/folders/new')(sequelize); import foldersNewRoute from './routes/folders/new.js';
const foldersEditRoute = require('./routes/folders/edit')(sequelize); import foldersEditRoute from './routes/folders/edit.js';
const foldersDeleteRoute = require('./routes/folders/delete')(sequelize); import foldersDeleteRoute from './routes/folders/delete.js';
app.use('/folders', foldersIndexRoute); app.use('/folders', foldersIndexRoute(sequelize));
app.use('/folders', foldersNewRoute); app.use('/folders', foldersNewRoute(sequelize));
app.use('/folders', foldersEditRoute); app.use('/folders', foldersEditRoute(sequelize));
app.use('/folders', foldersDeleteRoute); app.use('/folders', foldersDeleteRoute(sequelize));
// "/cases" // "/cases"
// "/cases" import casesDownloadRoute from './routes/cases/download.js';
const casesDownloadRoute = require('./routes/cases/download')(sequelize); import casesIndexRoute from './routes/cases/index.js';
const casesIndexRoute = require('./routes/cases/index')(sequelize); import casesIndexByProjectIdRoute from './routes/cases/indexByProjectId.js';
const casesIndexByProjectIdRoute = require('./routes/cases/indexByProjectId')(sequelize); import casesShowRoute from './routes/cases/show.js';
const casesShowRoute = require('./routes/cases/show')(sequelize); import casesNewRoute from './routes/cases/new.js';
const casesNewRoute = require('./routes/cases/new')(sequelize); import casesEditRoute from './routes/cases/edit.js';
const casesEditRoute = require('./routes/cases/edit')(sequelize); import casesDeleteRoute from './routes/cases/delete.js';
const casesDeleteRoute = require('./routes/cases/delete')(sequelize); app.use('/cases', casesDownloadRoute(sequelize));
app.use('/cases', casesDownloadRoute); app.use('/cases', casesIndexRoute(sequelize));
app.use('/cases', casesIndexRoute); app.use('/cases', casesIndexByProjectIdRoute(sequelize));
app.use('/cases', casesIndexByProjectIdRoute); app.use('/cases', casesShowRoute(sequelize));
app.use('/cases', casesShowRoute); app.use('/cases', casesNewRoute(sequelize));
app.use('/cases', casesNewRoute); app.use('/cases', casesEditRoute(sequelize));
app.use('/cases', casesEditRoute); app.use('/cases', casesDeleteRoute(sequelize));
app.use('/cases', casesDeleteRoute);
// "/steps" // "/steps"
const stepsEditRoute = require('./routes/steps/edit')(sequelize); import stepsEditRoute from './routes/steps/edit.js';
app.use('/steps', stepsEditRoute); app.use('/steps', stepsEditRoute(sequelize));
// "/attachments" // "/attachments"
const attachmentsNewRoute = require('./routes/attachments/new')(sequelize); import attachmentsNewRoute from './routes/attachments/new.js';
const attachmentsDeleteRoute = require('./routes/attachments/delete')(sequelize); import attachmentsDeleteRoute from './routes/attachments/delete.js';
const attachmentsDownloadRoute = require('./routes/attachments/download')(sequelize); import attachmentsDownloadRoute from './routes/attachments/download.js';
app.use('/attachments', attachmentsNewRoute); app.use('/attachments', attachmentsNewRoute(sequelize));
app.use('/attachments', attachmentsDeleteRoute); app.use('/attachments', attachmentsDeleteRoute(sequelize));
app.use('/attachments', attachmentsDownloadRoute); app.use('/attachments', attachmentsDownloadRoute(sequelize));
// "/runs" // "/runs"
const runsDownloadRoute = require('./routes/runs/download')(sequelize); import runsDownloadRoute from './routes/runs/download.js';
const runsIndexRoute = require('./routes/runs/index')(sequelize); import runsIndexRoute from './routes/runs/index.js';
const runsShowRoute = require('./routes/runs/show')(sequelize); import runsShowRoute from './routes/runs/show.js';
const runsNewRoute = require('./routes/runs/new')(sequelize); import runsNewRoute from './routes/runs/new.js';
const runsEditRoute = require('./routes/runs/edit')(sequelize); import runsEditRoute from './routes/runs/edit.js';
const runDeleteRoute = require('./routes/runs/delete')(sequelize); import runDeleteRoute from './routes/runs/delete.js';
app.use('/runs', runsDownloadRoute);
app.use('/runs', runsIndexRoute); app.use('/runs', runsDownloadRoute(sequelize));
app.use('/runs', runsShowRoute); app.use('/runs', runsIndexRoute(sequelize));
app.use('/runs', runsNewRoute); app.use('/runs', runsShowRoute(sequelize));
app.use('/runs', runsEditRoute); app.use('/runs', runsNewRoute(sequelize));
app.use('/runs', runDeleteRoute); app.use('/runs', runsEditRoute(sequelize));
app.use('/runs', runDeleteRoute(sequelize));
// "/runcases" // "/runcases"
const runCaseIndexRoute = require('./routes/runcases/index')(sequelize); import runCaseIndexRoute from './routes/runcases/index.js';
const runCaseEditRoute = require('./routes/runcases/edit')(sequelize); import runCaseEditRoute from './routes/runcases/edit.js';
app.use('/runcases', runCaseIndexRoute); app.use('/runcases', runCaseIndexRoute(sequelize));
app.use('/runcases', runCaseEditRoute); app.use('/runcases', runCaseEditRoute(sequelize));
// "/members" // "/members"
const membersIndexRoute = require('./routes/members/index')(sequelize); import membersIndexRoute from './routes/members/index.js';
const membersNewRoute = require('./routes/members/new')(sequelize); import membersNewRoute from './routes/members/new.js';
const membersEditRoute = require('./routes/members/edit')(sequelize); import membersEditRoute from './routes/members/edit.js';
const membersDeleteRoute = require('./routes/members/delete')(sequelize); import membersDeleteRoute from './routes/members/delete.js';
const membersCheckRoute = require('./routes/members/check')(sequelize); import membersCheckRoute from './routes/members/check.js';
app.use('/members', membersIndexRoute); app.use('/members', membersIndexRoute(sequelize));
app.use('/members', membersNewRoute); app.use('/members', membersNewRoute(sequelize));
app.use('/members', membersEditRoute); app.use('/members', membersEditRoute(sequelize));
app.use('/members', membersDeleteRoute); app.use('/members', membersDeleteRoute(sequelize));
app.use('/members', membersCheckRoute); app.use('/members', membersCheckRoute(sequelize));
// "/home" // "/home"
const homeIndexRoute = require('./routes/home/index')(sequelize); import homeIndexRoute from './routes/home/index.js';
app.use('/home', homeIndexRoute); app.use('/home', homeIndexRoute(sequelize));
if (!process.env.SECRET_KEY) { if (!process.env.SECRET_KEY) {
console.log( console.log(
@@ -154,4 +152,4 @@ if (!process.env.SECRET_KEY) {
} }
// Export the app instead of starting the server // Export the app instead of starting the server
module.exports = app; export default app;

View File

@@ -1,9 +1,15 @@
import { createServer as createHttpServer } from 'http';
import path from 'path';
import fs from 'fs';
import { execSync } from 'child_process';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Use express from backend node_modules // Use express from backend node_modules
const { createServer: createHttpServer } = require('http'); import expressModule from './backend/node_modules/express/index.js';
const path = require('path'); const express = expressModule.default || expressModule;
const fs = require('fs');
const { execSync } = require('child_process');
const express = require('./backend/node_modules/express');
async function runMigrations() { async function runMigrations() {
try { try {
@@ -13,9 +19,7 @@ async function runMigrations() {
cwd: path.join(__dirname, 'backend'), cwd: path.join(__dirname, 'backend'),
stdio: 'inherit', stdio: 'inherit',
}); });
console.log('Database migrations completed successfully.'); console.log('Database migrations completed successfully.');
if (process.env.IS_DEMO === 'true' || process.env.IS_DEMO === '1') { if (process.env.IS_DEMO === 'true' || process.env.IS_DEMO === '1') {
console.log('Demo mode detected. Seeding the database...'); console.log('Demo mode detected. Seeding the database...');
execSync('npx sequelize-cli db:seed:all', { execSync('npx sequelize-cli db:seed:all', {
@@ -30,15 +34,14 @@ async function runMigrations() {
} }
} }
function startServer() { async function startServer() {
try { try {
const server = express(); const server = express();
const httpServer = createHttpServer(server); const httpServer = createHttpServer(server);
// Import the backend app // Import the backend app
const backendApp = require('./backend/server'); const backendAppModule = await import('./backend/server.js');
const backendApp = backendAppModule.default || backendAppModule;
// Use the backend app for /api routes
server.use('/api', backendApp); server.use('/api', backendApp);
// For Next.js standalone build // For Next.js standalone build
@@ -46,21 +49,18 @@ function startServer() {
const nextServerPath = './node_modules/next/dist/server/next.js'; const nextServerPath = './node_modules/next/dist/server/next.js';
if (fs.existsSync(nextServerPath)) { if (fs.existsSync(nextServerPath)) {
// Import Next.js // Import Next.js
const next = require(nextServerPath); const nextModule = await import(nextServerPath);
const next = nextModule.default || nextModule;
// Initialize Next.js app // Initialize Next.js app
const dev = process.env.NODE_ENV !== 'production'; const dev = process.env.NODE_ENV !== 'production';
const nextApp = next({ dev, dir: path.join(__dirname, '.') }); const nextApp = next({ dev, dir: path.join(__dirname, '.') });
const handle = nextApp.getRequestHandler(); const handle = nextApp.getRequestHandler();
await nextApp.prepare();
// Prepare Next.js
nextApp.prepare().then(() => {
console.log('nextjs prepared'); console.log('nextjs prepared');
// Use Next.js to handle all other routes // Use Next.js to handle all other routes
server.all('*', (req, res) => { server.all('*', (req, res) => handle(req, res));
return handle(req, res);
});
});
} else { } else {
console.error('Next.js module not found at:', nextServerPath); console.error('Next.js module not found at:', nextServerPath);
server.all('*', (req, res) => { server.all('*', (req, res) => {
@@ -79,9 +79,11 @@ function startServer() {
} }
} }
runMigrations().then(() => { runMigrations()
.then(() => {
startServer(); startServer();
}).catch(error => { })
.catch((error) => {
console.error('Failed to start application:', error); console.error('Failed to start application:', error);
process.exit(1); process.exit(1);
}); });

View File

@@ -12,7 +12,7 @@ import eslintPluginNext from '@next/eslint-plugin-next';
export default tseslint.config( export default tseslint.config(
{ {
name: 'unittcms/ignore-globally', name: 'unittcms/ignore-globally',
ignores: ['**/node_modules/', '**/.next/', '**/docs/'], ignores: ['**/node_modules/', '**/.next/', '**/docs/', '**/coverage/'],
}, },
{ {
name: 'unittcms/load-plugins', name: 'unittcms/load-plugins',

View File

@@ -203,7 +203,14 @@
"case_title": "Test Case Title", "case_title": "Test Case Title",
"case_description": "Test Case Description", "case_description": "Test Case Description",
"create": "Create", "create": "Create",
"please_enter": "Please enter test case title" "please_enter": "Please enter test case title",
"filter": "Filter",
"clear_all": "Clear all",
"apply": "Apply",
"select_priorities": "Select priorities",
"selected": "Selected",
"type": "Type",
"select_types": "Select types"
}, },
"Case": { "Case": {
"back_to_cases": "Back to test cases", "back_to_cases": "Back to test cases",

View File

@@ -204,7 +204,14 @@
"case_title": "テストケースタイトル", "case_title": "テストケースタイトル",
"case_description": "テストケース詳細", "case_description": "テストケース詳細",
"create": "作成", "create": "作成",
"please_enter": "テストケースタイトルを入力してください" "please_enter": "テストケースタイトルを入力してください",
"filter": "フィルター",
"clear_all": "すべてクリア",
"apply": "適用",
"select_priorities": "優先度を選択",
"selected": "選択済み",
"type": "タイプ",
"select_types": "タイプを選択"
}, },
"Case": { "Case": {
"back_to_cases": "テストケース一覧に戻る", "back_to_cases": "テストケース一覧に戻る",

View File

@@ -203,7 +203,14 @@
"case_title": "Título do Caso de Teste", "case_title": "Título do Caso de Teste",
"case_description": "Descrição do Caso de Teste", "case_description": "Descrição do Caso de Teste",
"create": "Criar", "create": "Criar",
"please_enter": "Por favor, insira o título do caso de teste" "please_enter": "Por favor, insira o título do caso de teste",
"filter": "Filtrar",
"clear_all": "Limpar tudo",
"apply": "Aplicar",
"select_priorities": "Selecionar prioridades",
"selected": "Selecionado",
"type": "Tipo",
"select_types": "Selecionar tipos"
}, },
"Case": { "Case": {
"back_to_cases": "Voltar para os casos de teste", "back_to_cases": "Voltar para os casos de teste",

View File

@@ -62,7 +62,7 @@ export default function HealthPage({ messages, locale }: Props) {
<TableBody> <TableBody>
<TableRow key="1"> <TableRow key="1">
<TableCell>{messages.unittcms_version}</TableCell> <TableCell>{messages.unittcms_version}</TableCell>
<TableCell>1.0.0-beta.18</TableCell> <TableCell>1.0.0-beta.19</TableCell>
</TableRow> </TableRow>
<TableRow key="2"> <TableRow key="2">
<TableCell>{messages.api_server}</TableCell> <TableCell>{messages.api_server}</TableCell>

View File

@@ -2,6 +2,7 @@ import { Button } from '@heroui/react';
import { ChevronDown, ChevronRight, Folder, Plus } from 'lucide-react'; import { ChevronDown, ChevronRight, Folder, Plus } from 'lucide-react';
import { NodeApi } from 'react-arborist'; import { NodeApi } from 'react-arborist';
import { useContext } from 'react'; import { useContext } from 'react';
import { useSearchParams } from 'next/navigation';
import FolderEditMenu from './FolderEditMenu'; import FolderEditMenu from './FolderEditMenu';
import { FolderType, FoldersMessages, TreeNodeData } from '@/types/folder'; import { FolderType, FoldersMessages, TreeNodeData } from '@/types/folder';
@@ -33,6 +34,7 @@ export default function FolderItem({
onDeleteClick, onDeleteClick,
}: FolderItemProps) { }: FolderItemProps) {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams();
const context = useContext(TokenContext); const context = useContext(TokenContext);
const isSelected = selectedFolder && node.data.folderData.id === selectedFolder.id; const isSelected = selectedFolder && node.data.folderData.id === selectedFolder.id;
@@ -69,11 +71,18 @@ export default function FolderItem({
</> </>
); );
const handleClick = () => {
const currentParams = new URLSearchParams(searchParams.toString());
const queryString = currentParams.toString();
const url = `/projects/${projectId}/folders/${node.data.folderData.id}/cases${queryString ? `?${queryString}` : ''}`;
router.push(url, { locale });
};
return ( return (
<TreeItem <TreeItem
style={style} style={style}
isSelected={isSelected} isSelected={isSelected}
onClick={() => router.push(`/projects/${projectId}/folders/${node.data.folderData.id}/cases`, { locale })} onClick={() => handleClick()}
toggleButton={toggleButton} toggleButton={toggleButton}
icon={<Folder size={20} color="#F7C24E" fill="#F7C24E" className="flex-shrink-0" />} icon={<Folder size={20} color="#F7C24E" fill="#F7C24E" className="flex-shrink-0" />}
label={node.data.name} label={node.data.name}

View File

@@ -1,5 +1,6 @@
'use client'; 'use client';
import { useState, useEffect, useContext } from 'react'; import { useState, useEffect, useContext } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import TestCaseTable from './TestCaseTable'; import TestCaseTable from './TestCaseTable';
import CaseDialog from './CaseDialog'; import CaseDialog from './CaseDialog';
import { TokenContext } from '@/utils/TokenProvider'; import { TokenContext } from '@/utils/TokenProvider';
@@ -7,29 +8,85 @@ import { fetchCases, createCase, deleteCases, exportCases } from '@/utils/caseCo
import { CaseType, CasesMessages } from '@/types/case'; import { CaseType, CasesMessages } from '@/types/case';
import DeleteConfirmDialog from '@/components/DeleteConfirmDialog'; import DeleteConfirmDialog from '@/components/DeleteConfirmDialog';
import { PriorityMessages } from '@/types/priority'; import { PriorityMessages } from '@/types/priority';
import { TestTypeMessages } from '@/types/testType';
import { LocaleCodeType } from '@/types/locale'; import { LocaleCodeType } from '@/types/locale';
import { logError } from '@/utils/errorHandler'; import { logError } from '@/utils/errorHandler';
import { parseQueryParam } from '@/utils/parseQueryParam';
type Props = { type Props = {
projectId: string; projectId: string;
folderId: string; folderId: string;
messages: CasesMessages; messages: CasesMessages;
priorityMessages: PriorityMessages; priorityMessages: PriorityMessages;
testTypeMessages: TestTypeMessages;
locale: LocaleCodeType; locale: LocaleCodeType;
}; };
export default function CasesPane({ projectId, folderId, messages, priorityMessages, locale }: Props) { export default function CasesPane({
projectId,
folderId,
messages,
priorityMessages,
testTypeMessages,
locale,
}: Props) {
const [cases, setCases] = useState<CaseType[]>([]); const [cases, setCases] = useState<CaseType[]>([]);
const context = useContext(TokenContext);
const [isCaseDialogOpen, setIsCaseDialogOpen] = useState(false); const [isCaseDialogOpen, setIsCaseDialogOpen] = useState(false);
const [titleFilter, setTitleFilter] = useState('');
const [priorityFilter, setPriorityFilter] = useState<number[]>([]);
const [typeFilter, setTypeFilter] = useState<number[]>([]);
const [isDeleteConfirmDialogOpen, setIsDeleteConfirmDialogOpen] = useState(false);
const [deleteCaseIds, setDeleteCaseIds] = useState<number[]>([]);
const context = useContext(TokenContext);
const router = useRouter();
const searchParams = useSearchParams();
const updateUrlParams = (updates: { title?: string; priority?: number[]; type?: number[] }) => {
const currentParams = new URLSearchParams(searchParams.toString());
if (updates.title) {
currentParams.set('title', updates.title);
} else {
currentParams.delete('title');
}
if (updates.priority && updates.priority.length > 0) {
currentParams.set('priority', updates.priority.join(','));
} else {
currentParams.delete('priority');
}
if (updates.type && updates.type.length > 0) {
currentParams.set('type', updates.type.join(','));
} else {
currentParams.delete('type');
}
const newUrl = `${window.location.pathname}?${currentParams.toString()}`;
router.push(newUrl, { scroll: false });
};
useEffect(() => { useEffect(() => {
async function fetchDataEffect() { async function fetchDataEffect() {
if (!context.isSignedIn()) { if (!context.isSignedIn()) return;
return;
} const titleParam = searchParams.get('title') || '';
const priorityParam = parseQueryParam(searchParams.get('priority'));
const typeParam = parseQueryParam(searchParams.get('type'));
setTitleFilter(titleParam);
setPriorityFilter(priorityParam);
setTypeFilter(typeParam);
try { try {
const data = await fetchCases(context.token.access_token, Number(folderId)); const data = await fetchCases(
context.token.access_token,
Number(folderId),
titleParam || undefined,
priorityParam.length > 0 ? priorityParam : undefined,
typeParam.length > 0 ? typeParam : undefined
);
setCases(data); setCases(data);
} catch (error: unknown) { } catch (error: unknown) {
logError('Error fetching cases:', error); logError('Error fetching cases:', error);
@@ -37,11 +94,9 @@ export default function CasesPane({ projectId, folderId, messages, priorityMessa
} }
fetchDataEffect(); fetchDataEffect();
}, [context, folderId]); }, [context, folderId, searchParams]);
const closeDialog = () => { const closeDialog = () => setIsCaseDialogOpen(false);
setIsCaseDialogOpen(false);
};
const onSubmit = async (title: string, description: string) => { const onSubmit = async (title: string, description: string) => {
const newCase = await createCase(context.token.access_token, folderId, title, description); const newCase = await createCase(context.token.access_token, folderId, title, description);
@@ -49,15 +104,12 @@ export default function CasesPane({ projectId, folderId, messages, priorityMessa
closeDialog(); closeDialog();
}; };
// Delete confirm dialog
const [isDeleteConfirmDialogOpen, setIsDeleteConfirmDialogOpen] = useState(false);
const [deleteCaseIds, setDeleteCaseIds] = useState<number[]>([]);
const closeDeleteConfirmDialog = () => { const closeDeleteConfirmDialog = () => {
setIsDeleteConfirmDialogOpen(false); setIsDeleteConfirmDialogOpen(false);
setDeleteCaseIds([]); setDeleteCaseIds([]);
}; };
const onDeleteCase = async (deleteCaseId: number) => { const onDeleteCase = (deleteCaseId: number) => {
setDeleteCaseIds([deleteCaseId]); setDeleteCaseIds([deleteCaseId]);
setIsDeleteConfirmDialogOpen(true); setIsDeleteConfirmDialogOpen(true);
}; };
@@ -79,6 +131,13 @@ export default function CasesPane({ projectId, folderId, messages, priorityMessa
await exportCases(context.token.access_token, Number(folderId), type); await exportCases(context.token.access_token, Number(folderId), type);
}; };
const handleFilterChange = (title: string, priorities: number[], types: number[]) => {
setTitleFilter(title);
setPriorityFilter(priorities);
setTypeFilter(types);
updateUrlParams({ title: title, priority: priorities, type: types });
};
return ( return (
<> <>
<TestCaseTable <TestCaseTable
@@ -89,8 +148,13 @@ export default function CasesPane({ projectId, folderId, messages, priorityMessa
onDeleteCase={onDeleteCase} onDeleteCase={onDeleteCase}
onDeleteCases={onDeleteCases} onDeleteCases={onDeleteCases}
onExportCases={onExportCases} onExportCases={onExportCases}
onFilterChange={handleFilterChange}
activeTitleFilter={titleFilter}
activePriorityFilters={priorityFilter}
activeTypeFilters={typeFilter}
messages={messages} messages={messages}
priorityMessages={priorityMessages} priorityMessages={priorityMessages}
testTypeMessages={testTypeMessages}
locale={locale} locale={locale}
/> />

View File

@@ -0,0 +1,173 @@
import { useState, useEffect } from 'react';
import { Button, Dropdown, DropdownTrigger, DropdownMenu, DropdownItem, Selection, Input } from '@heroui/react';
import { SearchIcon, ChevronDown, Circle } from 'lucide-react';
import { PriorityMessages } from '@/types/priority';
import { TestTypeMessages } from '@/types/testType';
import { CasesMessages } from '@/types/case';
import { priorities, testTypes } from '@/config/selection';
type TestCaseFilterProps = {
messages: CasesMessages;
priorityMessages: PriorityMessages;
testTypeMessages: TestTypeMessages;
activeTitleFilter: string;
activePriorityFilters: number[];
activeTypeFilters: number[];
onFilterChange: (title: string, priorities: number[], types: number[]) => void;
};
export default function TestCaseFilter({
messages,
priorityMessages,
testTypeMessages,
activeTitleFilter,
activePriorityFilters,
activeTypeFilters,
onFilterChange,
}: TestCaseFilterProps) {
const [title, setTitle] = useState<string>('');
const [selectedPriorities, setSelectedPriorities] = useState<Selection>(new Set([]));
const [selectedTypes, setSelectedTypes] = useState<Selection>(new Set([]));
useEffect(() => {
setTitle(activeTitleFilter);
}, [activeTitleFilter]);
useEffect(() => {
if (activePriorityFilters.length > 0) {
const activeKeys = activePriorityFilters.map((index) => priorities[index]?.uid).filter(Boolean);
setSelectedPriorities(new Set(activeKeys));
} else {
setSelectedPriorities(new Set([]));
}
}, [activePriorityFilters]);
useEffect(() => {
if (activeTypeFilters.length > 0) {
const activeKeys = activeTypeFilters.map((index) => testTypes[index]?.uid).filter(Boolean);
setSelectedTypes(new Set(activeKeys));
} else {
setSelectedTypes(new Set([]));
}
}, [activeTypeFilters]);
const handlePrioritySelectionChange = (keys: Selection) => {
setSelectedPriorities(keys);
};
const handleTypeSelectionChange = (keys: Selection) => {
setSelectedTypes(keys);
};
const handleApplyFilter = () => {
let priorityIndices: number[] = [];
if (selectedPriorities !== 'all' && selectedPriorities.size > 0) {
priorityIndices = Array.from(selectedPriorities)
.map((key) => priorities.findIndex((priority) => priority.uid === key))
.filter((index) => index !== -1);
}
let typeIndices: number[] = [];
if (selectedTypes !== 'all' && selectedTypes.size > 0) {
typeIndices = Array.from(selectedTypes)
.map((key) => testTypes.findIndex((type) => type.uid === key))
.filter((index) => index !== -1);
}
onFilterChange(title, priorityIndices, typeIndices);
};
const handleClearFilter = () => {
setSelectedPriorities(new Set([]));
setSelectedTypes(new Set([]));
onFilterChange('', [], []);
};
return (
<div className="p-3">
<div className="mb-3 space-y-1">
<h3 className="text-default-500 text-small">{messages.caseTitle}</h3>
<Input
variant="bordered"
classNames={{
base: 'max-w-full h-8',
mainWrapper: 'h-full',
input: 'text-small',
}}
size="sm"
startContent={<SearchIcon size={18} />}
type="search"
value={title}
onValueChange={setTitle}
maxLength={100}
/>
</div>
<div className="mb-3 flex justify-between gap-2">
<div className="flex-col space-y-1">
<h3 className="text-default-500 text-small">{messages.priority}</h3>
<Dropdown>
<DropdownTrigger>
<Button size="sm" variant="bordered" className="w-32" endContent={<ChevronDown size={16} />}>
{selectedPriorities === 'all' || selectedPriorities.size === 0
? messages.selectPriorities
: `${selectedPriorities.size} ${messages.selected || 'selected'}`}
</Button>
</DropdownTrigger>
<DropdownMenu
aria-label="Priority filter"
selectionMode="multiple"
selectedKeys={selectedPriorities}
onSelectionChange={handlePrioritySelectionChange}
>
{priorities.map((priority) => (
<DropdownItem
key={priority.uid}
textValue={priorityMessages[priority.uid]}
className="flex items-center"
>
<div className="flex items-center gap-2">
<Circle size={8} color={priority.color} fill={priority.color} />
<span className="text-sm">{priorityMessages[priority.uid]}</span>
</div>
</DropdownItem>
))}
</DropdownMenu>
</Dropdown>
</div>
<div className="flex-col space-y-1">
<h3 className="text-default-500 text-small">{messages.type}</h3>
<Dropdown>
<DropdownTrigger>
<Button size="sm" variant="bordered" className="w-32" endContent={<ChevronDown size={16} />}>
{selectedTypes === 'all' || selectedTypes.size === 0
? messages.selectTypes || 'Select Types'
: `${selectedTypes.size} ${messages.selected || 'selected'}`}
</Button>
</DropdownTrigger>
<DropdownMenu
className="max-h-[50vh] overflow-y-auto"
aria-label="Type filter"
selectionMode="multiple"
selectedKeys={selectedTypes}
onSelectionChange={handleTypeSelectionChange}
>
{testTypes.map((type) => (
<DropdownItem key={type.uid} textValue={testTypeMessages[type.uid]} className="flex items-center">
<span className="text-sm">{testTypeMessages[type.uid]}</span>
</DropdownItem>
))}
</DropdownMenu>
</Dropdown>
</div>
</div>
<div className="flex justify-end">
<Button className="me-2" size="sm" variant="light" onPress={handleClearFilter}>
{messages.clearAll}
</Button>
<Button size="sm" variant="solid" color="primary" onPress={handleApplyFilter}>
{messages.apply}
</Button>
</div>
</div>
);
}

View File

@@ -13,14 +13,20 @@ import {
DropdownItem, DropdownItem,
Selection, Selection,
SortDescriptor, SortDescriptor,
ButtonGroup, Badge,
Popover,
PopoverContent,
PopoverTrigger,
} from '@heroui/react'; } from '@heroui/react';
import { Plus, MoreVertical, Trash, FileDown, ChevronDown, FileJson, FileSpreadsheet } from 'lucide-react'; import { Plus, MoreVertical, Trash, FileDown, ChevronDown, Filter, FileJson, FileSpreadsheet } from 'lucide-react';
import TestCaseFilter from './TestCaseFilter';
import { Link } from '@/src/i18n/routing'; import { Link } from '@/src/i18n/routing';
import { CaseType, CasesMessages } from '@/types/case'; import { CaseType, CasesMessages } from '@/types/case';
import { PriorityMessages } from '@/types/priority'; import { PriorityMessages } from '@/types/priority';
import { TestTypeMessages } from '@/types/testType';
import TestCasePriority from '@/components/TestCasePriority'; import TestCasePriority from '@/components/TestCasePriority';
import { LocaleCodeType } from '@/types/locale'; import { LocaleCodeType } from '@/types/locale';
import { highlightSearchTerm } from '@/utils/highlightSearchTerm';
type Props = { type Props = {
projectId: string; projectId: string;
@@ -30,8 +36,13 @@ type Props = {
onDeleteCase: (caseId: number) => void; onDeleteCase: (caseId: number) => void;
onDeleteCases: (caseIds: number[]) => void; onDeleteCases: (caseIds: number[]) => void;
onExportCases: (type: string) => void; onExportCases: (type: string) => void;
onFilterChange: (query: string, priorities: number[], types: number[]) => void;
activeTitleFilter: string;
activePriorityFilters: number[];
activeTypeFilters: number[];
messages: CasesMessages; messages: CasesMessages;
priorityMessages: PriorityMessages; priorityMessages: PriorityMessages;
testTypeMessages: TestTypeMessages;
locale: LocaleCodeType; locale: LocaleCodeType;
}; };
@@ -43,8 +54,13 @@ export default function TestCaseTable({
onDeleteCase, onDeleteCase,
onDeleteCases, onDeleteCases,
onExportCases, onExportCases,
onFilterChange,
activeTitleFilter,
activePriorityFilters,
activeTypeFilters,
messages, messages,
priorityMessages, priorityMessages,
testTypeMessages,
locale, locale,
}: Props) { }: Props) {
const headerColumns = [ const headerColumns = [
@@ -59,7 +75,7 @@ export default function TestCaseTable({
column: 'id', column: 'id',
direction: 'ascending', direction: 'ascending',
}); });
const [exportType, setExportType] = useState(new Set(['json'])); const [showFilter, setShowFilter] = useState(false);
const sortedItems = useMemo(() => { const sortedItems = useMemo(() => {
if (cases.length === 0) { if (cases.length === 0) {
@@ -78,7 +94,8 @@ export default function TestCaseTable({
onDeleteCase(deleteCaseId); onDeleteCase(deleteCaseId);
}; };
const renderCell = useCallback((testCase: CaseType, columnKey: string): ReactNode => { const renderCell = useCallback(
(testCase: CaseType, columnKey: string): ReactNode => {
const cellValue = testCase[columnKey as keyof CaseType]; const cellValue = testCase[columnKey as keyof CaseType];
switch (columnKey) { switch (columnKey) {
@@ -92,9 +109,12 @@ export default function TestCaseTable({
href={`/projects/${projectId}/folders/${testCase.folderId}/cases/${testCase.id}`} href={`/projects/${projectId}/folders/${testCase.folderId}/cases/${testCase.id}`}
locale={locale} locale={locale}
variant="light" variant="light"
className="data-[hover=true]:bg-transparent" className="data-[hover=true]:bg-transparent gap-0"
> >
{cellValue as string} {highlightSearchTerm({
text: cellValue as string,
searchTerm: activeTitleFilter,
})}
</Button> </Button>
); );
case 'priority': case 'priority':
@@ -122,8 +142,10 @@ export default function TestCaseTable({
default: default:
return cellValue as string; return cellValue as string;
} }
},
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); [activeTitleFilter]
);
const handleDeleteCases = () => { const handleDeleteCases = () => {
let deleteCaseIds: number[]; let deleteCaseIds: number[];
@@ -136,9 +158,7 @@ export default function TestCaseTable({
setSelectedKeys(new Set([])); setSelectedKeys(new Set([]));
}; };
const handleExportTypeChange = (keys: Selection) => { const activeFilterNum = (activeTitleFilter ? 1 : 0) + activePriorityFilters.length + activeTypeFilters.length;
setExportType(new Set(Array.from(keys as Set<string>)));
};
const classNames = useMemo( const classNames = useMemo(
() => ({ () => ({
@@ -161,14 +181,15 @@ export default function TestCaseTable({
return ( return (
<> <>
<div className="border-b-1 dark:border-neutral-700 w-full p-3 flex items-center justify-between"> <div className="border-b-1 dark:border-neutral-700 w-full ">
<div className="flex items-center justify-between p-3 ">
<h3 className="font-bold">{messages.testCaseList}</h3> <h3 className="font-bold">{messages.testCaseList}</h3>
<div className="flex items-center">
<div>
{((selectedKeys !== 'all' && selectedKeys.size > 0) || selectedKeys === 'all') && ( {((selectedKeys !== 'all' && selectedKeys.size > 0) || selectedKeys === 'all') && (
<Button <Button
startContent={<Trash size={16} />} startContent={<Trash size={16} />}
size="sm" size="sm"
variant="bordered"
isDisabled={isDisabled} isDisabled={isDisabled}
color="danger" color="danger"
className="me-2" className="me-2"
@@ -177,37 +198,60 @@ export default function TestCaseTable({
{messages.delete} {messages.delete}
</Button> </Button>
)} )}
<ButtonGroup className="me-2"> <Popover placement="bottom" isOpen={showFilter} onOpenChange={(open) => setShowFilter(open)}>
<Button <Badge
startContent={<FileDown size={16} />} color="danger"
size="sm" content={activeFilterNum}
onPress={() => onExportCases(Array.from(exportType)[0])} isInvisible={activeFilterNum === 0}
shape="circle"
placement="top-left"
> >
{messages.export} {exportType} <PopoverTrigger>
<Button startContent={<Filter size={16} />} size="sm" variant="bordered" className="me-2">
{messages.filter}
</Button> </Button>
<Dropdown placement="bottom-end"> </PopoverTrigger>
</Badge>
<PopoverContent>
<TestCaseFilter
messages={messages}
priorityMessages={priorityMessages}
testTypeMessages={testTypeMessages}
activeTitleFilter={activeTitleFilter}
activePriorityFilters={activePriorityFilters}
activeTypeFilters={activeTypeFilters}
onFilterChange={(newTitleFilter, newPriorityFilters, newTypeFilters) => {
setShowFilter(false);
onFilterChange(newTitleFilter, newPriorityFilters, newTypeFilters);
}}
/>
</PopoverContent>
</Popover>
<Dropdown>
<DropdownTrigger> <DropdownTrigger>
<Button isIconOnly size="sm"> <Button
<ChevronDown size={16} /> size="sm"
variant="bordered"
className="me-2"
startContent={<FileDown size={16} />}
endContent={<ChevronDown size={16} />}
>
{messages.export}
</Button> </Button>
</DropdownTrigger> </DropdownTrigger>
<DropdownMenu <DropdownMenu aria-label="Export options">
disallowEmptySelection <DropdownItem key="json" startContent={<FileJson size={16} />} onPress={() => onExportCases('json')}>
aria-label="Export options"
className="max-w-[300px]"
selectedKeys={exportType}
selectionMode="single"
onSelectionChange={handleExportTypeChange}
>
<DropdownItem key="json" startContent={<FileJson size={16} />}>
json json
</DropdownItem> </DropdownItem>
<DropdownItem key="csv" startContent={<FileSpreadsheet size={16} />}> <DropdownItem
key="csv"
startContent={<FileSpreadsheet size={16} />}
onPress={() => onExportCases('csv')}
>
csv csv
</DropdownItem> </DropdownItem>
</DropdownMenu> </DropdownMenu>
</Dropdown> </Dropdown>
</ButtonGroup>
<Button <Button
startContent={<Plus size={16} />} startContent={<Plus size={16} />}
size="sm" size="sm"
@@ -219,11 +263,12 @@ export default function TestCaseTable({
</Button> </Button>
</div> </div>
</div> </div>
</div>
<Table <Table
isCompact isCompact
removeWrapper removeWrapper
aria-label="Tese cases table" aria-label="Test cases table"
classNames={classNames} classNames={classNames}
selectedKeys={selectedKeys} selectedKeys={selectedKeys}
selectionMode="multiple" selectionMode="multiple"

View File

@@ -2,6 +2,7 @@ import { getTranslations } from 'next-intl/server';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import CasesPane from './CasesPane'; import CasesPane from './CasesPane';
import { PriorityMessages } from '@/types/priority'; import { PriorityMessages } from '@/types/priority';
import { TestTypeMessages } from '@/types/testType';
import { LocaleCodeType } from '@/types/locale'; import { LocaleCodeType } from '@/types/locale';
export async function generateMetadata({ params: { locale } }: { params: { locale: LocaleCodeType } }) { export async function generateMetadata({ params: { locale } }: { params: { locale: LocaleCodeType } }) {
@@ -32,6 +33,13 @@ export default function Page({ params }: { params: { projectId: string; folderId
caseDescription: t('case_description'), caseDescription: t('case_description'),
create: t('create'), create: t('create'),
pleaseEnter: t('please_enter'), pleaseEnter: t('please_enter'),
apply: t('apply'),
filter: t('filter'),
clearAll: t('clear_all'),
selectPriorities: t('select_priorities'),
selected: t('selected'),
type: t('type'),
selectTypes: t('select_types'),
}; };
const priorityTranslation = useTranslations('Priority'); const priorityTranslation = useTranslations('Priority');
@@ -42,6 +50,23 @@ export default function Page({ params }: { params: { projectId: string; folderId
low: priorityTranslation('low'), low: priorityTranslation('low'),
}; };
const testTypeTranslation = useTranslations('Type');
const testTypeMessages: TestTypeMessages = {
other: testTypeTranslation('other'),
security: testTypeTranslation('security'),
performance: testTypeTranslation('performance'),
accessibility: testTypeTranslation('accessibility'),
functional: testTypeTranslation('functional'),
acceptance: testTypeTranslation('acceptance'),
usability: testTypeTranslation('usability'),
smokeSanity: testTypeTranslation('smoke_sanity'),
compatibility: testTypeTranslation('compatibility'),
destructive: testTypeTranslation('destructive'),
regression: testTypeTranslation('regression'),
automated: testTypeTranslation('automated'),
manual: testTypeTranslation('manual'),
};
return ( return (
<> <>
<CasesPane <CasesPane
@@ -50,6 +75,7 @@ export default function Page({ params }: { params: { projectId: string; folderId
locale={params.locale as LocaleCodeType} locale={params.locale as LocaleCodeType}
messages={messages} messages={messages}
priorityMessages={priorityMessages} priorityMessages={priorityMessages}
testTypeMessages={testTypeMessages}
/> />
</> </>
); );

View File

@@ -14,7 +14,6 @@ import {
DropdownMenu, DropdownMenu,
DropdownItem, DropdownItem,
addToast, addToast,
ButtonGroup,
Badge, Badge,
} from '@heroui/react'; } from '@heroui/react';
import { import {
@@ -103,7 +102,6 @@ export default function RunEditor({
const [isNameInvalid] = useState<boolean>(false); const [isNameInvalid] = useState<boolean>(false);
const [isUpdating, setIsUpdating] = useState<boolean>(false); const [isUpdating, setIsUpdating] = useState<boolean>(false);
const [isDirty, setIsDirty] = useState(false); const [isDirty, setIsDirty] = useState(false);
const [exportType, setExportType] = useState(new Set(['xml']));
const router = useRouter(); const router = useRouter();
useFormGuard(isDirty, messages.areYouSureLeave); useFormGuard(isDirty, messages.areYouSureLeave);
@@ -201,10 +199,6 @@ export default function RunEditor({
setIsDirty(false); setIsDirty(false);
}; };
const handleExportTypeChange = (keys: Selection) => {
setExportType(new Set(Array.from(keys as Set<string>)));
};
return ( return (
<> <>
<div className="border-b-1 dark:border-neutral-700 w-full p-3 flex items-center justify-between"> <div className="border-b-1 dark:border-neutral-700 w-full p-3 flex items-center justify-between">
@@ -222,40 +216,42 @@ export default function RunEditor({
<h3 className="font-bold ms-2">{testRun.name}</h3> <h3 className="font-bold ms-2">{testRun.name}</h3>
</div> </div>
<div className="flex items-center"> <div className="flex items-center">
<ButtonGroup className="me-2">
<Button
startContent={<FileDown size={16} />}
size="sm"
onPress={() => exportRun(tokenContext.token.access_token, Number(testRun.id), Array.from(exportType)[0])}
>
{messages.export} {exportType}
</Button>
<Dropdown placement="bottom-end"> <Dropdown placement="bottom-end">
<DropdownTrigger> <DropdownTrigger>
<Button isIconOnly size="sm"> <Button
<ChevronDown size={16} /> variant="bordered"
size="sm"
className="me-2"
startContent={<FileDown size={16} />}
endContent={<ChevronDown size={16} />}
>
{messages.export}
</Button> </Button>
</DropdownTrigger> </DropdownTrigger>
<DropdownMenu <DropdownMenu disallowEmptySelection aria-label="Export options">
disallowEmptySelection <DropdownItem
aria-label="Export options" key="xml"
className="max-w-[300px]" startContent={<FileCode size={16} />}
selectedKeys={exportType} onPress={() => exportRun(tokenContext.token.access_token, Number(testRun.id), 'xml')}
selectionMode="single"
onSelectionChange={handleExportTypeChange}
> >
<DropdownItem key="xml" startContent={<FileCode size={16} />}>
xml xml
</DropdownItem> </DropdownItem>
<DropdownItem key="json" startContent={<FileJson size={16} />}> <DropdownItem
key="json"
startContent={<FileJson size={16} />}
onPress={() => exportRun(tokenContext.token.access_token, Number(testRun.id), 'json')}
>
json json
</DropdownItem> </DropdownItem>
<DropdownItem key="csv" startContent={<FileSpreadsheet size={16} />}> <DropdownItem
key="csv"
startContent={<FileSpreadsheet size={16} />}
onPress={() => exportRun(tokenContext.token.access_token, Number(testRun.id), 'csv')}
>
csv csv
</DropdownItem> </DropdownItem>
</DropdownMenu> </DropdownMenu>
</Dropdown> </Dropdown>
</ButtonGroup>
<Button <Button
startContent={ startContent={
<Badge isInvisible={!isDirty} color="danger" size="sm" content="" shape="circle"> <Badge isInvisible={!isDirty} color="danger" size="sm" content="" shape="circle">

View File

@@ -77,6 +77,13 @@ type CasesMessages = {
caseDescription: string; caseDescription: string;
create: string; create: string;
pleaseEnter: string; pleaseEnter: string;
filter: string;
clearAll: string;
apply: string;
selectPriorities: string;
selected: string;
type: string;
selectTypes: string;
}; };
type CaseMessages = { type CaseMessages = {

View File

@@ -26,8 +26,24 @@ async function fetchCase(jwt: string, caseId: number) {
} }
} }
async function fetchCases(jwt: string, folderId: number) { async function fetchCases(jwt: string, folderId: number, title?: string, priority?: number[], type?: number[]) {
const url = `${apiServer}/cases?folderId=${folderId}`; const queryParams = [`folderId=${folderId}`];
if (title) {
queryParams.push(`title=${title}`);
}
if (priority && priority.length > 0) {
queryParams.push(`priority=${priority.join(',')}`);
}
if (type && type.length > 0) {
queryParams.push(`type=${type.join(',')}`);
}
const query = queryParams.length > 0 ? `?${queryParams.join('&')}` : '';
const url = `${apiServer}/cases${query}`;
try { try {
const response = await fetch(url, { const response = await fetch(url, {
@@ -43,9 +59,10 @@ async function fetchCases(jwt: string, folderId: number) {
} }
const data = await response.json(); const data = await response.json();
return data; return data || [];
} catch (error: unknown) { } catch (error: unknown) {
logError('Error fetching data', error); logError('Error fetching data', error);
return [];
} }
} }

View File

@@ -0,0 +1,38 @@
import { cn } from '@heroui/theme';
import { ReactNode } from 'react';
interface HighlightSearchTermProps {
text: string;
searchTerm: string;
className?: string;
minSearchLength?: number;
}
export function highlightSearchTerm({
text,
searchTerm,
className,
minSearchLength = 1,
}: HighlightSearchTermProps): ReactNode {
if (!text || !searchTerm || searchTerm.length < minSearchLength) {
return text || null;
}
const regex = new RegExp(`(${searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
const parts = text.split(regex);
return (
<>
{parts.map((part, index) => {
if (regex.test(part)) {
return (
<mark key={`${part}-${index}`} className={cn(className, 'bg-yellow-200 dark:bg-yellow-60 py-0.5 rounded')}>
{part}
</mark>
);
}
return part;
})}
</>
);
}

View File

@@ -0,0 +1,8 @@
export const parseQueryParam = (param: string | null): number[] => {
return param
? param
.split(',')
.map((p) => parseInt(p.trim()))
.filter((p) => !isNaN(p))
: [];
};

View File

@@ -0,0 +1,30 @@
import { useRef, useCallback, useEffect } from 'react';
export default function useDebounce<T extends (...args: unknown[]) => void>(
fn: T,
delay: number
): (...args: Parameters<T>) => void {
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const debounceFn = useCallback(
(...args: Parameters<T>): void => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
fn(...args);
}, delay);
},
[fn, delay]
);
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
return debounceFn;
}

97
package-lock.json generated
View File

@@ -24,7 +24,7 @@
"prettier": "^3.3.3", "prettier": "^3.3.3",
"sequelize": "^6.37.4", "sequelize": "^6.37.4",
"sqlite3": "^5.1.7", "sqlite3": "^5.1.7",
"supertest": "^7.0.0", "supertest": "^7.1.4",
"typescript-eslint": "^8.24.1", "typescript-eslint": "^8.24.1",
"vitest": "^1.6.0" "vitest": "^1.6.0"
} }
@@ -803,6 +803,19 @@
"fast-glob": "3.3.1" "fast-glob": "3.3.1"
} }
}, },
"node_modules/@noble/hashes": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -873,6 +886,16 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/@paralleldrive/cuid2": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz",
"integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@noble/hashes": "^1.1.5"
}
},
"node_modules/@playwright/test": { "node_modules/@playwright/test": {
"version": "1.51.1", "version": "1.51.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.51.1.tgz", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.51.1.tgz",
@@ -2123,7 +2146,8 @@
"version": "2.0.6", "version": "2.0.6",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
"dev": true "dev": true,
"license": "MIT"
}, },
"node_modules/assertion-error": { "node_modules/assertion-error": {
"version": "1.1.0", "version": "1.1.0",
@@ -2153,7 +2177,8 @@
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true "dev": true,
"license": "MIT"
}, },
"node_modules/available-typed-arrays": { "node_modules/available-typed-arrays": {
"version": "1.0.7", "version": "1.0.7",
@@ -2521,6 +2546,7 @@
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"delayed-stream": "~1.0.0" "delayed-stream": "~1.0.0"
}, },
@@ -2533,6 +2559,7 @@
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
"integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==",
"dev": true, "dev": true,
"license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
@@ -2596,7 +2623,8 @@
"version": "2.1.4", "version": "2.1.4",
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz",
"integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==",
"dev": true "dev": true,
"license": "MIT"
}, },
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
@@ -2767,6 +2795,7 @@
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=0.4.0" "node": ">=0.4.0"
} }
@@ -2811,6 +2840,7 @@
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
"integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==",
"dev": true, "dev": true,
"license": "ISC",
"dependencies": { "dependencies": {
"asap": "^2.0.0", "asap": "^2.0.0",
"wrappy": "1" "wrappy": "1"
@@ -3762,7 +3792,8 @@
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
"dev": true "dev": true,
"license": "MIT"
}, },
"node_modules/fastq": { "node_modules/fastq": {
"version": "1.19.1", "version": "1.19.1",
@@ -3887,14 +3918,16 @@
} }
}, },
"node_modules/form-data": { "node_modules/form-data": {
"version": "4.0.2", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"asynckit": "^0.4.0", "asynckit": "^0.4.0",
"combined-stream": "^1.0.8", "combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0", "es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12" "mime-types": "^2.1.12"
}, },
"engines": { "engines": {
@@ -3902,15 +3935,19 @@
} }
}, },
"node_modules/formidable": { "node_modules/formidable": {
"version": "3.5.2", "version": "3.5.4",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.2.tgz", "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz",
"integrity": "sha512-Jqc1btCy3QzRbJaICGwKcBfGWuLADRerLzDqi2NwSt/UkXLsHJw2TVResiaoBufHVHy9aSgClOHCeJsSsFLTbg==", "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@paralleldrive/cuid2": "^2.2.2",
"dezalgo": "^1.0.4", "dezalgo": "^1.0.4",
"hexoid": "^2.0.0",
"once": "^1.4.0" "once": "^1.4.0"
}, },
"engines": {
"node": ">=14.0.0"
},
"funding": { "funding": {
"url": "https://ko-fi.com/tunnckoCore/commissions" "url": "https://ko-fi.com/tunnckoCore/commissions"
} }
@@ -4303,15 +4340,6 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/hexoid": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/hexoid/-/hexoid-2.0.0.tgz",
"integrity": "sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/html-escaper": { "node_modules/html-escaper": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
@@ -7310,20 +7338,21 @@
"dev": true "dev": true
}, },
"node_modules/superagent": { "node_modules/superagent": {
"version": "9.0.2", "version": "10.2.3",
"resolved": "https://registry.npmjs.org/superagent/-/superagent-9.0.2.tgz", "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.3.tgz",
"integrity": "sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==", "integrity": "sha512-y/hkYGeXAj7wUMjxRbB21g/l6aAEituGXM9Rwl4o20+SX3e8YOSV6BxFXl+dL3Uk0mjSL3kCbNkwURm8/gEDig==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"component-emitter": "^1.3.0", "component-emitter": "^1.3.1",
"cookiejar": "^2.1.4", "cookiejar": "^2.1.4",
"debug": "^4.3.4", "debug": "^4.3.7",
"fast-safe-stringify": "^2.1.1", "fast-safe-stringify": "^2.1.1",
"form-data": "^4.0.0", "form-data": "^4.0.4",
"formidable": "^3.5.1", "formidable": "^3.5.4",
"methods": "^1.1.2", "methods": "^1.1.2",
"mime": "2.6.0", "mime": "2.6.0",
"qs": "^6.11.0" "qs": "^6.11.2"
}, },
"engines": { "engines": {
"node": ">=14.18.0" "node": ">=14.18.0"
@@ -7334,6 +7363,7 @@
"resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
"integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
"dev": true, "dev": true,
"license": "MIT",
"bin": { "bin": {
"mime": "cli.js" "mime": "cli.js"
}, },
@@ -7342,13 +7372,14 @@
} }
}, },
"node_modules/supertest": { "node_modules/supertest": {
"version": "7.1.0", "version": "7.1.4",
"resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.0.tgz", "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.4.tgz",
"integrity": "sha512-5QeSO8hSrKghtcWEoPiO036fxH0Ii2wVQfFZSP0oqQhmjk8bOLhDFXr4JrvaFmPuEWUoq4znY3uSi8UzLKxGqw==", "integrity": "sha512-tjLPs7dVyqgItVFirHYqe2T+MfWc2VOBQ8QFKKbWTA3PU7liZR8zoSpAi/C1k1ilm9RsXIKYf197oap9wXGVYg==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"methods": "^1.1.2", "methods": "^1.1.2",
"superagent": "^9.0.1" "superagent": "^10.2.3"
}, },
"engines": { "engines": {
"node": ">=14.18.0" "node": ">=14.18.0"

View File

@@ -2,6 +2,7 @@
"name": "unittcms", "name": "unittcms",
"version": "1.0.0", "version": "1.0.0",
"private": true, "private": true,
"type": "module",
"scripts": { "scripts": {
"format:check": "prettier --check frontend/**/*.{js,ts,jsx,tsx,json} backend/**/*.{js,ts,json}", "format:check": "prettier --check frontend/**/*.{js,ts,jsx,tsx,json} backend/**/*.{js,ts,json}",
"format": "prettier --write frontend/**/*.{js,ts,jsx,tsx,json} backend/**/*.{js,ts,json}", "format": "prettier --write frontend/**/*.{js,ts,jsx,tsx,json} backend/**/*.{js,ts,json}",
@@ -29,7 +30,7 @@
"prettier": "^3.3.3", "prettier": "^3.3.3",
"sequelize": "^6.37.4", "sequelize": "^6.37.4",
"sqlite3": "^5.1.7", "sqlite3": "^5.1.7",
"supertest": "^7.0.0", "supertest": "^7.1.4",
"typescript-eslint": "^8.24.1", "typescript-eslint": "^8.24.1",
"vitest": "^1.6.0" "vitest": "^1.6.0"
} }