fixed transitions errors
This commit is contained in:
parent
991ac75f32
commit
b2f641a398
@ -5,9 +5,14 @@
|
|||||||
"start": "npm run db:migrate && npm run db:seed && npm run watch",
|
"start": "npm run db:migrate && npm run db:seed && npm run watch",
|
||||||
"lint": "eslint . --ext .js",
|
"lint": "eslint . --ext .js",
|
||||||
"db:migrate": "sequelize-cli db:migrate",
|
"db:migrate": "sequelize-cli db:migrate",
|
||||||
|
"db:migrate:undo": "sequelize-cli db:migrate:undo",
|
||||||
|
"db:migrate:undo:all": "sequelize-cli db:migrate:undo:all",
|
||||||
|
"db:migrate:status": "sequelize-cli db:migrate:status",
|
||||||
"db:seed": "sequelize-cli db:seed:all",
|
"db:seed": "sequelize-cli db:seed:all",
|
||||||
|
"db:seed:undo": "sequelize-cli db:seed:undo:all",
|
||||||
"db:drop": "sequelize-cli db:drop",
|
"db:drop": "sequelize-cli db:drop",
|
||||||
"db:create": "sequelize-cli db:create",
|
"db:create": "sequelize-cli db:create",
|
||||||
|
"db:reset": "npm run db:drop && npm run db:create && npm run db:migrate && npm run db:seed",
|
||||||
"watch": "node watcher.js"
|
"watch": "node watcher.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@ -19,11 +19,11 @@ class AssetsDBApi extends GenericDBApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static get RANGE_FIELDS() {
|
static get RANGE_FIELDS() {
|
||||||
return ['size_mb', 'width_px', 'height_px', 'duration_sec', 'deleted_at_time'];
|
return ['size_mb', 'width_px', 'height_px', 'duration_sec'];
|
||||||
}
|
}
|
||||||
|
|
||||||
static get ENUM_FIELDS() {
|
static get ENUM_FIELDS() {
|
||||||
return ['asset_type', 'type', 'is_public', 'is_deleted'];
|
return ['asset_type', 'type', 'is_public'];
|
||||||
}
|
}
|
||||||
|
|
||||||
static get CSV_FIELDS() {
|
static get CSV_FIELDS() {
|
||||||
@ -68,8 +68,6 @@ class AssetsDBApi extends GenericDBApi {
|
|||||||
duration_sec: data.duration_sec || null,
|
duration_sec: data.duration_sec || null,
|
||||||
checksum: data.checksum || null,
|
checksum: data.checksum || null,
|
||||||
is_public: data.is_public || false,
|
is_public: data.is_public || false,
|
||||||
is_deleted: data.is_deleted || false,
|
|
||||||
deleted_at_time: data.deleted_at_time || null,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -133,14 +133,12 @@ class GenericDBApi {
|
|||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
await db.sequelize.transaction(async (tx) => {
|
for (const record of records) {
|
||||||
for (const record of records) {
|
await record.update({ deletedBy: currentUser.id }, { transaction });
|
||||||
await record.update({ deletedBy: currentUser.id }, { transaction: tx });
|
}
|
||||||
}
|
for (const record of records) {
|
||||||
for (const record of records) {
|
await record.destroy({ transaction });
|
||||||
await record.destroy({ transaction: tx });
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return records;
|
return records;
|
||||||
}
|
}
|
||||||
@ -163,11 +161,14 @@ class GenericDBApi {
|
|||||||
|
|
||||||
static async findBy(where, options = {}) {
|
static async findBy(where, options = {}) {
|
||||||
const transaction = options.transaction;
|
const transaction = options.transaction;
|
||||||
|
const include = options.include !== undefined
|
||||||
|
? options.include
|
||||||
|
: this.FIND_BY_INCLUDES;
|
||||||
|
|
||||||
const record = await this.MODEL.findOne({
|
const record = await this.MODEL.findOne({
|
||||||
where,
|
where,
|
||||||
transaction,
|
transaction,
|
||||||
include: this.FIND_BY_INCLUDES,
|
include,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!record) {
|
if (!record) {
|
||||||
|
|||||||
@ -23,11 +23,11 @@ class ProjectsDBApi extends GenericDBApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static get RANGE_FIELDS() {
|
static get RANGE_FIELDS() {
|
||||||
return ['deleted_at_time'];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
static get ENUM_FIELDS() {
|
static get ENUM_FIELDS() {
|
||||||
return ['phase', 'is_deleted'];
|
return ['phase'];
|
||||||
}
|
}
|
||||||
|
|
||||||
static get CSV_FIELDS() {
|
static get CSV_FIELDS() {
|
||||||
@ -56,11 +56,27 @@ class ProjectsDBApi extends GenericDBApi {
|
|||||||
custom_css_json: data.custom_css_json || null,
|
custom_css_json: data.custom_css_json || null,
|
||||||
cdn_base_url: data.cdn_base_url || null,
|
cdn_base_url: data.cdn_base_url || null,
|
||||||
entry_page_slug: data.entry_page_slug || null,
|
entry_page_slug: data.entry_page_slug || null,
|
||||||
is_deleted: data.is_deleted || false,
|
|
||||||
deleted_at_time: data.deleted_at_time || null,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static get DEFAULT_INCLUDES() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
static get ALL_INCLUDES() {
|
||||||
|
return [
|
||||||
|
{ association: 'project_memberships_project' },
|
||||||
|
{ association: 'assets_project' },
|
||||||
|
{ association: 'presigned_url_requests_project' },
|
||||||
|
{ association: 'tour_pages_project' },
|
||||||
|
{ association: 'transitions_project' },
|
||||||
|
{ association: 'project_audio_tracks_project' },
|
||||||
|
{ association: 'publish_events_project' },
|
||||||
|
{ association: 'pwa_caches_project' },
|
||||||
|
{ association: 'access_logs_project' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
static async findBy(where, options = {}) {
|
static async findBy(where, options = {}) {
|
||||||
const transaction = options.transaction;
|
const transaction = options.transaction;
|
||||||
const runtimeEnvironment = getRuntimeEnvironment(options);
|
const runtimeEnvironment = getRuntimeEnvironment(options);
|
||||||
@ -77,20 +93,14 @@ class ProjectsDBApi extends GenericDBApi {
|
|||||||
queryWhere.slug = runtimeProjectSlug;
|
queryWhere.slug = runtimeProjectSlug;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const include = options.include !== undefined
|
||||||
|
? options.include
|
||||||
|
: this.DEFAULT_INCLUDES;
|
||||||
|
|
||||||
const record = await this.MODEL.findOne({
|
const record = await this.MODEL.findOne({
|
||||||
where: queryWhere,
|
where: queryWhere,
|
||||||
transaction,
|
transaction,
|
||||||
include: [
|
include,
|
||||||
{ association: 'project_memberships_project' },
|
|
||||||
{ association: 'assets_project' },
|
|
||||||
{ association: 'presigned_url_requests_project' },
|
|
||||||
{ association: 'tour_pages_project' },
|
|
||||||
{ association: 'transitions_project' },
|
|
||||||
{ association: 'project_audio_tracks_project' },
|
|
||||||
{ association: 'publish_events_project' },
|
|
||||||
{ association: 'pwa_caches_project' },
|
|
||||||
{ association: 'access_logs_project' },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!record) return null;
|
if (!record) return null;
|
||||||
|
|||||||
@ -175,7 +175,7 @@ class Ui_elementsDBApi extends GenericDBApi {
|
|||||||
const now = new Date();
|
const now = new Date();
|
||||||
await this.MODEL.bulkCreate(
|
await this.MODEL.bulkCreate(
|
||||||
this.DEFAULT_ROWS.map((item) => ({
|
this.DEFAULT_ROWS.map((item) => ({
|
||||||
...item,
|
...this.getFieldMapping(item),
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
})),
|
})),
|
||||||
|
|||||||
@ -10,6 +10,8 @@ module.exports = {
|
|||||||
port: process.env.DB_PORT,
|
port: process.env.DB_PORT,
|
||||||
logging: false,
|
logging: false,
|
||||||
seederStorage: 'sequelize',
|
seederStorage: 'sequelize',
|
||||||
|
migrationStorage: 'sequelize',
|
||||||
|
migrationStorageTableName: 'SequelizeMeta',
|
||||||
},
|
},
|
||||||
development: {
|
development: {
|
||||||
username: 'postgres',
|
username: 'postgres',
|
||||||
@ -19,6 +21,8 @@ module.exports = {
|
|||||||
host: process.env.DB_HOST || 'localhost',
|
host: process.env.DB_HOST || 'localhost',
|
||||||
logging: console.log,
|
logging: console.log,
|
||||||
seederStorage: 'sequelize',
|
seederStorage: 'sequelize',
|
||||||
|
migrationStorage: 'sequelize',
|
||||||
|
migrationStorageTableName: 'SequelizeMeta',
|
||||||
},
|
},
|
||||||
dev_stage: {
|
dev_stage: {
|
||||||
dialect: 'postgres',
|
dialect: 'postgres',
|
||||||
@ -29,5 +33,7 @@ module.exports = {
|
|||||||
port: process.env.DB_PORT,
|
port: process.env.DB_PORT,
|
||||||
logging: console.log,
|
logging: console.log,
|
||||||
seederStorage: 'sequelize',
|
seederStorage: 'sequelize',
|
||||||
|
migrationStorage: 'sequelize',
|
||||||
|
migrationStorageTableName: 'SequelizeMeta',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,150 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migration to add foreign key constraints to all model associations.
|
||||||
|
* This enforces referential integrity at the database level.
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface) {
|
||||||
|
const transaction = await queryInterface.sequelize.transaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Helper to add FK constraint safely (checks if exists first)
|
||||||
|
const addForeignKey = async (tableName, columnName, references, onDelete = 'CASCADE', onUpdate = 'CASCADE') => {
|
||||||
|
const constraintName = `${tableName}_${columnName}_fkey`;
|
||||||
|
|
||||||
|
// Check if constraint already exists
|
||||||
|
const [results] = await queryInterface.sequelize.query(
|
||||||
|
`SELECT constraint_name FROM information_schema.table_constraints
|
||||||
|
WHERE table_name = '${tableName}' AND constraint_name = '${constraintName}'`,
|
||||||
|
{ transaction }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
await queryInterface.addConstraint(tableName, {
|
||||||
|
fields: [columnName],
|
||||||
|
type: 'foreign key',
|
||||||
|
name: constraintName,
|
||||||
|
references: {
|
||||||
|
table: references.table,
|
||||||
|
field: references.field,
|
||||||
|
},
|
||||||
|
onDelete,
|
||||||
|
onUpdate,
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
console.log(`Added FK constraint: ${constraintName}`);
|
||||||
|
} else {
|
||||||
|
console.log(`FK constraint already exists: ${constraintName}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// asset_variants -> assets
|
||||||
|
await addForeignKey('asset_variants', 'assetId', { table: 'assets', field: 'id' }, 'CASCADE', 'CASCADE');
|
||||||
|
|
||||||
|
// page_elements -> tour_pages
|
||||||
|
await addForeignKey('page_elements', 'pageId', { table: 'tour_pages', field: 'id' }, 'CASCADE', 'CASCADE');
|
||||||
|
|
||||||
|
// page_links -> tour_pages (from_page)
|
||||||
|
await addForeignKey('page_links', 'from_pageId', { table: 'tour_pages', field: 'id' }, 'CASCADE', 'CASCADE');
|
||||||
|
|
||||||
|
// page_links -> tour_pages (to_page)
|
||||||
|
await addForeignKey('page_links', 'to_pageId', { table: 'tour_pages', field: 'id' }, 'SET NULL', 'CASCADE');
|
||||||
|
|
||||||
|
// page_links -> transitions
|
||||||
|
await addForeignKey('page_links', 'transitionId', { table: 'transitions', field: 'id' }, 'SET NULL', 'CASCADE');
|
||||||
|
|
||||||
|
// assets -> projects
|
||||||
|
await addForeignKey('assets', 'projectId', { table: 'projects', field: 'id' }, 'CASCADE', 'CASCADE');
|
||||||
|
|
||||||
|
// tour_pages -> projects
|
||||||
|
await addForeignKey('tour_pages', 'projectId', { table: 'projects', field: 'id' }, 'CASCADE', 'CASCADE');
|
||||||
|
|
||||||
|
// transitions -> projects
|
||||||
|
await addForeignKey('transitions', 'projectId', { table: 'projects', field: 'id' }, 'CASCADE', 'CASCADE');
|
||||||
|
|
||||||
|
// project_memberships -> projects
|
||||||
|
await addForeignKey('project_memberships', 'projectId', { table: 'projects', field: 'id' }, 'CASCADE', 'CASCADE');
|
||||||
|
|
||||||
|
// project_memberships -> users
|
||||||
|
await addForeignKey('project_memberships', 'userId', { table: 'users', field: 'id' }, 'CASCADE', 'CASCADE');
|
||||||
|
|
||||||
|
// presigned_url_requests -> projects
|
||||||
|
await addForeignKey('presigned_url_requests', 'projectId', { table: 'projects', field: 'id' }, 'CASCADE', 'CASCADE');
|
||||||
|
|
||||||
|
// presigned_url_requests -> users
|
||||||
|
await addForeignKey('presigned_url_requests', 'userId', { table: 'users', field: 'id' }, 'CASCADE', 'CASCADE');
|
||||||
|
|
||||||
|
// project_audio_tracks -> projects
|
||||||
|
await addForeignKey('project_audio_tracks', 'projectId', { table: 'projects', field: 'id' }, 'CASCADE', 'CASCADE');
|
||||||
|
|
||||||
|
// publish_events -> projects
|
||||||
|
await addForeignKey('publish_events', 'projectId', { table: 'projects', field: 'id' }, 'CASCADE', 'CASCADE');
|
||||||
|
|
||||||
|
// publish_events -> users (SET NULL to preserve audit trail)
|
||||||
|
await addForeignKey('publish_events', 'userId', { table: 'users', field: 'id' }, 'SET NULL', 'CASCADE');
|
||||||
|
|
||||||
|
// pwa_caches -> projects
|
||||||
|
await addForeignKey('pwa_caches', 'projectId', { table: 'projects', field: 'id' }, 'CASCADE', 'CASCADE');
|
||||||
|
|
||||||
|
// access_logs -> projects
|
||||||
|
await addForeignKey('access_logs', 'projectId', { table: 'projects', field: 'id' }, 'CASCADE', 'CASCADE');
|
||||||
|
|
||||||
|
// access_logs -> users (SET NULL to preserve audit trail)
|
||||||
|
await addForeignKey('access_logs', 'userId', { table: 'users', field: 'id' }, 'SET NULL', 'CASCADE');
|
||||||
|
|
||||||
|
// users -> roles (SET NULL so deleting role doesn't delete users)
|
||||||
|
await addForeignKey('users', 'app_roleId', { table: 'roles', field: 'id' }, 'SET NULL', 'CASCADE');
|
||||||
|
|
||||||
|
await transaction.commit();
|
||||||
|
console.log('All FK constraints added successfully');
|
||||||
|
} catch (error) {
|
||||||
|
await transaction.rollback();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface) {
|
||||||
|
const transaction = await queryInterface.sequelize.transaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dropForeignKey = async (tableName, columnName) => {
|
||||||
|
const constraintName = `${tableName}_${columnName}_fkey`;
|
||||||
|
try {
|
||||||
|
await queryInterface.removeConstraint(tableName, constraintName, { transaction });
|
||||||
|
console.log(`Removed FK constraint: ${constraintName}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`FK constraint not found (may not exist): ${constraintName}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove all FK constraints in reverse order
|
||||||
|
await dropForeignKey('users', 'app_roleId');
|
||||||
|
await dropForeignKey('access_logs', 'userId');
|
||||||
|
await dropForeignKey('access_logs', 'projectId');
|
||||||
|
await dropForeignKey('pwa_caches', 'projectId');
|
||||||
|
await dropForeignKey('publish_events', 'userId');
|
||||||
|
await dropForeignKey('publish_events', 'projectId');
|
||||||
|
await dropForeignKey('project_audio_tracks', 'projectId');
|
||||||
|
await dropForeignKey('presigned_url_requests', 'userId');
|
||||||
|
await dropForeignKey('presigned_url_requests', 'projectId');
|
||||||
|
await dropForeignKey('project_memberships', 'userId');
|
||||||
|
await dropForeignKey('project_memberships', 'projectId');
|
||||||
|
await dropForeignKey('transitions', 'projectId');
|
||||||
|
await dropForeignKey('tour_pages', 'projectId');
|
||||||
|
await dropForeignKey('assets', 'projectId');
|
||||||
|
await dropForeignKey('page_links', 'transitionId');
|
||||||
|
await dropForeignKey('page_links', 'to_pageId');
|
||||||
|
await dropForeignKey('page_links', 'from_pageId');
|
||||||
|
await dropForeignKey('page_elements', 'pageId');
|
||||||
|
await dropForeignKey('asset_variants', 'assetId');
|
||||||
|
|
||||||
|
await transaction.commit();
|
||||||
|
console.log('All FK constraints removed successfully');
|
||||||
|
} catch (error) {
|
||||||
|
await transaction.rollback();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,100 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migration to remove redundant deletion tracking columns.
|
||||||
|
*
|
||||||
|
* The `is_deleted` and `deleted_at_time` columns are redundant because:
|
||||||
|
* - Sequelize's `paranoid: true` mode already uses `deletedAt` for soft-delete
|
||||||
|
* - These columns were set but never queried for filtering
|
||||||
|
*
|
||||||
|
* IMPORTANT: This migration should only be run after verifying no external
|
||||||
|
* systems depend on these columns. Consider backing up data first.
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface) {
|
||||||
|
const transaction = await queryInterface.sequelize.transaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Helper to safely remove column if it exists
|
||||||
|
const removeColumnIfExists = async (tableName, columnName) => {
|
||||||
|
const [results] = await queryInterface.sequelize.query(
|
||||||
|
`SELECT column_name FROM information_schema.columns
|
||||||
|
WHERE table_name = '${tableName}' AND column_name = '${columnName}'`,
|
||||||
|
{ transaction }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (results.length > 0) {
|
||||||
|
await queryInterface.removeColumn(tableName, columnName, { transaction });
|
||||||
|
console.log(`Removed column: ${tableName}.${columnName}`);
|
||||||
|
} else {
|
||||||
|
console.log(`Column does not exist (skipping): ${tableName}.${columnName}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove is_deleted index from assets first (if exists)
|
||||||
|
try {
|
||||||
|
await queryInterface.removeIndex('assets', 'assets_is_deleted', { transaction });
|
||||||
|
console.log('Removed index: assets_is_deleted');
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Index assets_is_deleted not found (may not exist)');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove redundant columns from assets table
|
||||||
|
await removeColumnIfExists('assets', 'is_deleted');
|
||||||
|
await removeColumnIfExists('assets', 'deleted_at_time');
|
||||||
|
|
||||||
|
// Remove redundant columns from projects table
|
||||||
|
await removeColumnIfExists('projects', 'is_deleted');
|
||||||
|
await removeColumnIfExists('projects', 'deleted_at_time');
|
||||||
|
|
||||||
|
await transaction.commit();
|
||||||
|
console.log('Redundant deletion columns removed successfully');
|
||||||
|
} catch (error) {
|
||||||
|
await transaction.rollback();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async down(queryInterface, Sequelize) {
|
||||||
|
const transaction = await queryInterface.sequelize.transaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Re-add columns to assets table
|
||||||
|
await queryInterface.addColumn('assets', 'is_deleted', {
|
||||||
|
type: Sequelize.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: false,
|
||||||
|
}, { transaction });
|
||||||
|
|
||||||
|
await queryInterface.addColumn('assets', 'deleted_at_time', {
|
||||||
|
type: Sequelize.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
}, { transaction });
|
||||||
|
|
||||||
|
// Re-add index
|
||||||
|
await queryInterface.addIndex('assets', ['is_deleted'], {
|
||||||
|
name: 'assets_is_deleted',
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-add columns to projects table
|
||||||
|
await queryInterface.addColumn('projects', 'is_deleted', {
|
||||||
|
type: Sequelize.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: false,
|
||||||
|
}, { transaction });
|
||||||
|
|
||||||
|
await queryInterface.addColumn('projects', 'deleted_at_time', {
|
||||||
|
type: Sequelize.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
}, { transaction });
|
||||||
|
|
||||||
|
await transaction.commit();
|
||||||
|
console.log('Redundant deletion columns restored successfully');
|
||||||
|
} catch (error) {
|
||||||
|
await transaction.rollback();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -105,7 +105,9 @@ accessed_at: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'projectId',
|
name: 'projectId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
});
|
});
|
||||||
|
|
||||||
db.access_logs.belongsTo(db.users, {
|
db.access_logs.belongsTo(db.users, {
|
||||||
@ -113,7 +115,9 @@ accessed_at: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'userId',
|
name: 'userId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -114,7 +114,9 @@ size_mb: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'assetId',
|
name: 'assetId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -137,23 +137,6 @@ is_public: {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
is_deleted: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: false,
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
deleted_at_time: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
importHash: {
|
importHash: {
|
||||||
@ -171,7 +154,6 @@ deleted_at_time: {
|
|||||||
{ fields: ['asset_type'] },
|
{ fields: ['asset_type'] },
|
||||||
{ fields: ['type'] },
|
{ fields: ['type'] },
|
||||||
{ fields: ['is_public'] },
|
{ fields: ['is_public'] },
|
||||||
{ fields: ['is_deleted'] },
|
|
||||||
{ fields: ['deletedAt'] },
|
{ fields: ['deletedAt'] },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -194,7 +176,9 @@ deleted_at_time: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'assetId',
|
name: 'assetId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -217,7 +201,9 @@ deleted_at_time: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'projectId',
|
name: 'projectId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -163,7 +163,9 @@ content_json: {
|
|||||||
name: 'pageId',
|
name: 'pageId',
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -104,7 +104,9 @@ trigger_selector: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'from_pageId',
|
name: 'from_pageId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
});
|
});
|
||||||
|
|
||||||
db.page_links.belongsTo(db.tour_pages, {
|
db.page_links.belongsTo(db.tour_pages, {
|
||||||
@ -112,7 +114,9 @@ trigger_selector: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'to_pageId',
|
name: 'to_pageId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'SET NULL',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
});
|
});
|
||||||
|
|
||||||
db.page_links.belongsTo(db.transitions, {
|
db.page_links.belongsTo(db.transitions, {
|
||||||
@ -120,7 +124,9 @@ trigger_selector: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'transitionId',
|
name: 'transitionId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'SET NULL',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -131,7 +131,9 @@ status: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'projectId',
|
name: 'projectId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
});
|
});
|
||||||
|
|
||||||
db.presigned_url_requests.belongsTo(db.users, {
|
db.presigned_url_requests.belongsTo(db.users, {
|
||||||
@ -139,7 +141,9 @@ status: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'userId',
|
name: 'userId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -135,7 +135,9 @@ is_enabled: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'projectId',
|
name: 'projectId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -106,7 +106,9 @@ accepted_at: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'projectId',
|
name: 'projectId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
});
|
});
|
||||||
|
|
||||||
db.project_memberships.belongsTo(db.users, {
|
db.project_memberships.belongsTo(db.users, {
|
||||||
@ -114,7 +116,9 @@ accepted_at: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'userId',
|
name: 'userId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -95,23 +95,6 @@ entry_page_slug: {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
is_deleted: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: false,
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
deleted_at_time: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
importHash: {
|
importHash: {
|
||||||
@ -147,7 +130,9 @@ deleted_at_time: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'projectId',
|
name: 'projectId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -156,7 +141,9 @@ deleted_at_time: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'projectId',
|
name: 'projectId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -166,7 +153,9 @@ deleted_at_time: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'projectId',
|
name: 'projectId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -175,7 +164,9 @@ deleted_at_time: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'projectId',
|
name: 'projectId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -186,7 +177,9 @@ deleted_at_time: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'projectId',
|
name: 'projectId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -195,7 +188,9 @@ deleted_at_time: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'projectId',
|
name: 'projectId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -204,7 +199,9 @@ deleted_at_time: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'projectId',
|
name: 'projectId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -213,7 +210,9 @@ deleted_at_time: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'projectId',
|
name: 'projectId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -222,7 +221,9 @@ deleted_at_time: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'projectId',
|
name: 'projectId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -175,7 +175,9 @@ audios_copied: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'projectId',
|
name: 'projectId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
});
|
});
|
||||||
|
|
||||||
db.publish_events.belongsTo(db.users, {
|
db.publish_events.belongsTo(db.users, {
|
||||||
@ -183,7 +185,9 @@ audios_copied: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'userId',
|
name: 'userId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -104,7 +104,9 @@ is_active: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'projectId',
|
name: 'projectId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -44,7 +44,8 @@ role_customization: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'roles_permissionsId',
|
name: 'roles_permissionsId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
through: 'rolesPermissionsPermissions',
|
through: 'rolesPermissionsPermissions',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -53,7 +54,8 @@ role_customization: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'roles_permissionsId',
|
name: 'roles_permissionsId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
through: 'rolesPermissionsPermissions',
|
through: 'rolesPermissionsPermissions',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -66,7 +68,9 @@ role_customization: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'app_roleId',
|
name: 'app_roleId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'SET NULL',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -144,7 +144,9 @@ ui_schema_json: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'pageId',
|
name: 'pageId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -153,7 +155,9 @@ ui_schema_json: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'from_pageId',
|
name: 'from_pageId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
});
|
});
|
||||||
|
|
||||||
db.tour_pages.hasMany(db.page_links, {
|
db.tour_pages.hasMany(db.page_links, {
|
||||||
@ -161,7 +165,9 @@ ui_schema_json: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'to_pageId',
|
name: 'to_pageId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'SET NULL',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -180,7 +186,9 @@ ui_schema_json: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'projectId',
|
name: 'projectId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -123,7 +123,9 @@ duration_sec: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'transitionId',
|
name: 'transitionId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'SET NULL',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -142,7 +144,9 @@ duration_sec: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'projectId',
|
name: 'projectId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -130,7 +130,8 @@ provider: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'users_custom_permissionsId',
|
name: 'users_custom_permissionsId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
through: 'usersCustom_permissionsPermissions',
|
through: 'usersCustom_permissionsPermissions',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -139,7 +140,8 @@ provider: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'users_custom_permissionsId',
|
name: 'users_custom_permissionsId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
through: 'usersCustom_permissionsPermissions',
|
through: 'usersCustom_permissionsPermissions',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -156,7 +158,9 @@ provider: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'userId',
|
name: 'userId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -167,7 +171,9 @@ provider: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'userId',
|
name: 'userId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -181,7 +187,9 @@ provider: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'userId',
|
name: 'userId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'SET NULL',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -191,7 +199,9 @@ provider: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'userId',
|
name: 'userId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'SET NULL',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -205,7 +215,9 @@ provider: {
|
|||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'app_roleId',
|
name: 'app_roleId',
|
||||||
},
|
},
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'SET NULL',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -213,7 +225,9 @@ provider: {
|
|||||||
db.users.hasMany(db.file, {
|
db.users.hasMany(db.file, {
|
||||||
as: 'avatar',
|
as: 'avatar',
|
||||||
foreignKey: 'belongsToId',
|
foreignKey: 'belongsToId',
|
||||||
constraints: false,
|
constraints: true,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
scope: {
|
scope: {
|
||||||
belongsTo: db.users.getTableName(),
|
belongsTo: db.users.getTableName(),
|
||||||
belongsToColumn: 'avatar',
|
belongsToColumn: 'avatar',
|
||||||
|
|||||||
@ -1,9 +1,14 @@
|
|||||||
const db = require('./models');
|
const db = require('./models');
|
||||||
|
|
||||||
async function syncDatabase() {
|
async function syncDatabase() {
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
console.error('ERROR: sync.js should not be run in production. Use migrations instead.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('Syncing database...');
|
console.log('Syncing database...');
|
||||||
await db.sequelize.sync({ force: true });
|
await db.sequelize.sync({ alter: true });
|
||||||
console.log('Database synced successfully!');
|
console.log('Database synced successfully!');
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -1,11 +1,8 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { wrapAsync, commonErrorHandler } = require('../helpers');
|
const { wrapAsync, commonErrorHandler, isUuidV4 } = require('../helpers');
|
||||||
const { checkCrudPermissions } = require('../middlewares/check-permissions');
|
const { checkCrudPermissions } = require('../middlewares/check-permissions');
|
||||||
const { parse } = require('json2csv');
|
const { parse } = require('json2csv');
|
||||||
|
|
||||||
const isUuidV4 = (value) =>
|
|
||||||
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
|
||||||
|
|
||||||
function createEntityRouter(entityName, Service, DBApi, options = {}) {
|
function createEntityRouter(entityName, Service, DBApi, options = {}) {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
@ -70,14 +67,14 @@ function createEntityRouter(entityName, Service, DBApi, options = {}) {
|
|||||||
res.status(200).send(payload);
|
res.status(200).send(payload);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
router.get('/autocomplete', async (req, res) => {
|
router.get('/autocomplete', wrapAsync(async (req, res) => {
|
||||||
const payload = await DBApi.findAllAutocomplete(
|
const payload = await DBApi.findAllAutocomplete(
|
||||||
req.query.query,
|
req.query.query,
|
||||||
req.query.limit,
|
req.query.limit,
|
||||||
req.query.offset
|
req.query.offset
|
||||||
);
|
);
|
||||||
res.status(200).send(payload);
|
res.status(200).send(payload);
|
||||||
});
|
}));
|
||||||
|
|
||||||
router.get('/:id', wrapAsync(async (req, res) => {
|
router.get('/:id', wrapAsync(async (req, res) => {
|
||||||
if (!isUuidV4(req.params.id)) {
|
if (!isUuidV4(req.params.id)) {
|
||||||
|
|||||||
@ -22,4 +22,8 @@ module.exports = class Helpers {
|
|||||||
static jwtSign(data) {
|
static jwtSign(data) {
|
||||||
return jwt.sign(data, config.secret_key, {expiresIn: '6h'});
|
return jwt.sign(data, config.secret_key, {expiresIn: '6h'});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static isUuidV4(value) {
|
||||||
|
return /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,7 +3,7 @@ const express = require('express');
|
|||||||
|
|
||||||
const ProjectsService = require('../services/projects');
|
const ProjectsService = require('../services/projects');
|
||||||
const ProjectsDBApi = require('../db/api/projects');
|
const ProjectsDBApi = require('../db/api/projects');
|
||||||
const wrapAsync = require('../helpers').wrapAsync;
|
const { wrapAsync, isUuidV4 } = require('../helpers');
|
||||||
|
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
@ -17,9 +17,6 @@ const {
|
|||||||
|
|
||||||
router.use(checkCrudPermissions('projects'));
|
router.use(checkCrudPermissions('projects'));
|
||||||
|
|
||||||
const isUuidV4 = (value) =>
|
|
||||||
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
@ -328,11 +325,7 @@ router.get('/', wrapAsync(async (req, res) => {
|
|||||||
req.query, { currentUser, runtimeContext }
|
req.query, { currentUser, runtimeContext }
|
||||||
);
|
);
|
||||||
if (filetype && filetype === 'csv') {
|
if (filetype && filetype === 'csv') {
|
||||||
const fields = ['id','name','slug','description','logo_url','favicon_url','og_image_url','theme_config_json','custom_css_json','cdn_base_url','entry_page_slug',
|
const fields = ['id','name','slug','description','logo_url','favicon_url','og_image_url','theme_config_json','custom_css_json','cdn_base_url','entry_page_slug'];
|
||||||
|
|
||||||
|
|
||||||
'deleted_at_time',
|
|
||||||
];
|
|
||||||
const opts = { fields };
|
const opts = { fields };
|
||||||
try {
|
try {
|
||||||
const csv = parse(payload.rows, opts);
|
const csv = parse(payload.rows, opts);
|
||||||
|
|||||||
@ -1,136 +1,6 @@
|
|||||||
const db = require('../db/models');
|
|
||||||
const Access_logsDBApi = require('../db/api/access_logs');
|
const Access_logsDBApi = require('../db/api/access_logs');
|
||||||
const processFile = require("../middlewares/upload");
|
const { createEntityService } = require('../factories/service.factory');
|
||||||
const ValidationError = require('./notifications/errors/validation');
|
|
||||||
const csv = require('csv-parser');
|
|
||||||
const stream = require('stream');
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = class Access_logsService {
|
|
||||||
static async create(data, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
try {
|
|
||||||
await Access_logsDBApi.create(
|
|
||||||
data,
|
|
||||||
{
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async bulkImport(req, res) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await processFile(req, res);
|
|
||||||
const bufferStream = new stream.PassThrough();
|
|
||||||
const results = [];
|
|
||||||
|
|
||||||
await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
bufferStream
|
|
||||||
.pipe(csv())
|
|
||||||
.on('data', (data) => results.push(data))
|
|
||||||
.on('end', async () => {
|
|
||||||
console.log('CSV results', results);
|
|
||||||
resolve();
|
|
||||||
})
|
|
||||||
.on('error', (error) => reject(error));
|
|
||||||
})
|
|
||||||
|
|
||||||
await Access_logsDBApi.bulkImport(results, {
|
|
||||||
transaction,
|
|
||||||
ignoreDuplicates: true,
|
|
||||||
validate: true,
|
|
||||||
currentUser: req.currentUser
|
|
||||||
});
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async update(data, id, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
try {
|
|
||||||
let access_logs = await Access_logsDBApi.findBy(
|
|
||||||
{id},
|
|
||||||
{transaction},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!access_logs) {
|
|
||||||
throw new ValidationError(
|
|
||||||
'access_logsNotFound',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedAccess_logs = await Access_logsDBApi.update(
|
|
||||||
id,
|
|
||||||
data,
|
|
||||||
{
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
return updatedAccess_logs;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async deleteByIds(ids, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Access_logsDBApi.deleteByIds(ids, {
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
});
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async remove(id, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Access_logsDBApi.remove(
|
|
||||||
id,
|
|
||||||
{
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = createEntityService(Access_logsDBApi, {
|
||||||
|
entityName: 'access_logs',
|
||||||
|
});
|
||||||
|
|||||||
@ -1,136 +1,6 @@
|
|||||||
const db = require('../db/models');
|
|
||||||
const Asset_variantsDBApi = require('../db/api/asset_variants');
|
const Asset_variantsDBApi = require('../db/api/asset_variants');
|
||||||
const processFile = require("../middlewares/upload");
|
const { createEntityService } = require('../factories/service.factory');
|
||||||
const ValidationError = require('./notifications/errors/validation');
|
|
||||||
const csv = require('csv-parser');
|
|
||||||
const stream = require('stream');
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = class Asset_variantsService {
|
|
||||||
static async create(data, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
try {
|
|
||||||
await Asset_variantsDBApi.create(
|
|
||||||
data,
|
|
||||||
{
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async bulkImport(req, res) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await processFile(req, res);
|
|
||||||
const bufferStream = new stream.PassThrough();
|
|
||||||
const results = [];
|
|
||||||
|
|
||||||
await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
bufferStream
|
|
||||||
.pipe(csv())
|
|
||||||
.on('data', (data) => results.push(data))
|
|
||||||
.on('end', async () => {
|
|
||||||
console.log('CSV results', results);
|
|
||||||
resolve();
|
|
||||||
})
|
|
||||||
.on('error', (error) => reject(error));
|
|
||||||
})
|
|
||||||
|
|
||||||
await Asset_variantsDBApi.bulkImport(results, {
|
|
||||||
transaction,
|
|
||||||
ignoreDuplicates: true,
|
|
||||||
validate: true,
|
|
||||||
currentUser: req.currentUser
|
|
||||||
});
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async update(data, id, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
try {
|
|
||||||
let asset_variants = await Asset_variantsDBApi.findBy(
|
|
||||||
{id},
|
|
||||||
{transaction},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!asset_variants) {
|
|
||||||
throw new ValidationError(
|
|
||||||
'asset_variantsNotFound',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedAsset_variants = await Asset_variantsDBApi.update(
|
|
||||||
id,
|
|
||||||
data,
|
|
||||||
{
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
return updatedAsset_variants;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async deleteByIds(ids, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Asset_variantsDBApi.deleteByIds(ids, {
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
});
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async remove(id, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Asset_variantsDBApi.remove(
|
|
||||||
id,
|
|
||||||
{
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = createEntityService(Asset_variantsDBApi, {
|
||||||
|
entityName: 'asset_variants',
|
||||||
|
});
|
||||||
|
|||||||
@ -1,136 +1,6 @@
|
|||||||
const db = require('../db/models');
|
|
||||||
const AssetsDBApi = require('../db/api/assets');
|
const AssetsDBApi = require('../db/api/assets');
|
||||||
const processFile = require("../middlewares/upload");
|
const { createEntityService } = require('../factories/service.factory');
|
||||||
const ValidationError = require('./notifications/errors/validation');
|
|
||||||
const csv = require('csv-parser');
|
|
||||||
const stream = require('stream');
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = class AssetsService {
|
|
||||||
static async create(data, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
try {
|
|
||||||
await AssetsDBApi.create(
|
|
||||||
data,
|
|
||||||
{
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async bulkImport(req, res) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await processFile(req, res);
|
|
||||||
const bufferStream = new stream.PassThrough();
|
|
||||||
const results = [];
|
|
||||||
|
|
||||||
await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
bufferStream
|
|
||||||
.pipe(csv())
|
|
||||||
.on('data', (data) => results.push(data))
|
|
||||||
.on('end', async () => {
|
|
||||||
console.log('CSV results', results);
|
|
||||||
resolve();
|
|
||||||
})
|
|
||||||
.on('error', (error) => reject(error));
|
|
||||||
})
|
|
||||||
|
|
||||||
await AssetsDBApi.bulkImport(results, {
|
|
||||||
transaction,
|
|
||||||
ignoreDuplicates: true,
|
|
||||||
validate: true,
|
|
||||||
currentUser: req.currentUser
|
|
||||||
});
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async update(data, id, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
try {
|
|
||||||
let assets = await AssetsDBApi.findBy(
|
|
||||||
{id},
|
|
||||||
{transaction},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!assets) {
|
|
||||||
throw new ValidationError(
|
|
||||||
'assetsNotFound',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedAssets = await AssetsDBApi.update(
|
|
||||||
id,
|
|
||||||
data,
|
|
||||||
{
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
return updatedAssets;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async deleteByIds(ids, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await AssetsDBApi.deleteByIds(ids, {
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
});
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async remove(id, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await AssetsDBApi.remove(
|
|
||||||
id,
|
|
||||||
{
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = createEntityService(AssetsDBApi, {
|
||||||
|
entityName: 'assets',
|
||||||
|
});
|
||||||
|
|||||||
@ -186,6 +186,7 @@ const downloadLocal = async (req, res) => {
|
|||||||
if (!privateUrl) {
|
if (!privateUrl) {
|
||||||
return res.sendStatus(404);
|
return res.sendStatus(404);
|
||||||
}
|
}
|
||||||
|
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
|
||||||
res.download(path.join(config.uploadDir, privateUrl));
|
res.download(path.join(config.uploadDir, privateUrl));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -348,6 +349,7 @@ const downloadGCloud = async (req, res) => {
|
|||||||
const fileExists = await file.exists();
|
const fileExists = await file.exists();
|
||||||
|
|
||||||
if (fileExists[0]) {
|
if (fileExists[0]) {
|
||||||
|
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
|
||||||
const stream = file.createReadStream();
|
const stream = file.createReadStream();
|
||||||
stream.pipe(res);
|
stream.pipe(res);
|
||||||
}
|
}
|
||||||
@ -428,6 +430,7 @@ const downloadS3 = async (req, res) => {
|
|||||||
if (output.ContentType) {
|
if (output.ContentType) {
|
||||||
res.setHeader('Content-Type', output.ContentType);
|
res.setHeader('Content-Type', output.ContentType);
|
||||||
}
|
}
|
||||||
|
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
|
||||||
|
|
||||||
if (typeof output.Body.pipe === 'function') {
|
if (typeof output.Body.pipe === 'function') {
|
||||||
output.Body.pipe(res);
|
output.Body.pipe(res);
|
||||||
|
|||||||
@ -1,136 +1,6 @@
|
|||||||
const db = require('../db/models');
|
|
||||||
const Page_elementsDBApi = require('../db/api/page_elements');
|
const Page_elementsDBApi = require('../db/api/page_elements');
|
||||||
const processFile = require("../middlewares/upload");
|
const { createEntityService } = require('../factories/service.factory');
|
||||||
const ValidationError = require('./notifications/errors/validation');
|
|
||||||
const csv = require('csv-parser');
|
|
||||||
const stream = require('stream');
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = class Page_elementsService {
|
|
||||||
static async create(data, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
try {
|
|
||||||
await Page_elementsDBApi.create(
|
|
||||||
data,
|
|
||||||
{
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async bulkImport(req, res) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await processFile(req, res);
|
|
||||||
const bufferStream = new stream.PassThrough();
|
|
||||||
const results = [];
|
|
||||||
|
|
||||||
await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
bufferStream
|
|
||||||
.pipe(csv())
|
|
||||||
.on('data', (data) => results.push(data))
|
|
||||||
.on('end', async () => {
|
|
||||||
console.log('CSV results', results);
|
|
||||||
resolve();
|
|
||||||
})
|
|
||||||
.on('error', (error) => reject(error));
|
|
||||||
})
|
|
||||||
|
|
||||||
await Page_elementsDBApi.bulkImport(results, {
|
|
||||||
transaction,
|
|
||||||
ignoreDuplicates: true,
|
|
||||||
validate: true,
|
|
||||||
currentUser: req.currentUser
|
|
||||||
});
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async update(data, id, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
try {
|
|
||||||
let page_elements = await Page_elementsDBApi.findBy(
|
|
||||||
{id},
|
|
||||||
{transaction},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!page_elements) {
|
|
||||||
throw new ValidationError(
|
|
||||||
'page_elementsNotFound',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedPage_elements = await Page_elementsDBApi.update(
|
|
||||||
id,
|
|
||||||
data,
|
|
||||||
{
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
return updatedPage_elements;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async deleteByIds(ids, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Page_elementsDBApi.deleteByIds(ids, {
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
});
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async remove(id, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Page_elementsDBApi.remove(
|
|
||||||
id,
|
|
||||||
{
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = createEntityService(Page_elementsDBApi, {
|
||||||
|
entityName: 'page_elements',
|
||||||
|
});
|
||||||
|
|||||||
@ -1,136 +1,6 @@
|
|||||||
const db = require('../db/models');
|
|
||||||
const Page_linksDBApi = require('../db/api/page_links');
|
const Page_linksDBApi = require('../db/api/page_links');
|
||||||
const processFile = require("../middlewares/upload");
|
const { createEntityService } = require('../factories/service.factory');
|
||||||
const ValidationError = require('./notifications/errors/validation');
|
|
||||||
const csv = require('csv-parser');
|
|
||||||
const stream = require('stream');
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = class Page_linksService {
|
|
||||||
static async create(data, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
try {
|
|
||||||
await Page_linksDBApi.create(
|
|
||||||
data,
|
|
||||||
{
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async bulkImport(req, res) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await processFile(req, res);
|
|
||||||
const bufferStream = new stream.PassThrough();
|
|
||||||
const results = [];
|
|
||||||
|
|
||||||
await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
bufferStream
|
|
||||||
.pipe(csv())
|
|
||||||
.on('data', (data) => results.push(data))
|
|
||||||
.on('end', async () => {
|
|
||||||
console.log('CSV results', results);
|
|
||||||
resolve();
|
|
||||||
})
|
|
||||||
.on('error', (error) => reject(error));
|
|
||||||
})
|
|
||||||
|
|
||||||
await Page_linksDBApi.bulkImport(results, {
|
|
||||||
transaction,
|
|
||||||
ignoreDuplicates: true,
|
|
||||||
validate: true,
|
|
||||||
currentUser: req.currentUser
|
|
||||||
});
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async update(data, id, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
try {
|
|
||||||
let page_links = await Page_linksDBApi.findBy(
|
|
||||||
{id},
|
|
||||||
{transaction},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!page_links) {
|
|
||||||
throw new ValidationError(
|
|
||||||
'page_linksNotFound',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedPage_links = await Page_linksDBApi.update(
|
|
||||||
id,
|
|
||||||
data,
|
|
||||||
{
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
return updatedPage_links;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async deleteByIds(ids, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Page_linksDBApi.deleteByIds(ids, {
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
});
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async remove(id, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Page_linksDBApi.remove(
|
|
||||||
id,
|
|
||||||
{
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = createEntityService(Page_linksDBApi, {
|
||||||
|
entityName: 'page_links',
|
||||||
|
});
|
||||||
|
|||||||
@ -1,136 +1,6 @@
|
|||||||
const db = require('../db/models');
|
|
||||||
const PermissionsDBApi = require('../db/api/permissions');
|
const PermissionsDBApi = require('../db/api/permissions');
|
||||||
const processFile = require("../middlewares/upload");
|
const { createEntityService } = require('../factories/service.factory');
|
||||||
const ValidationError = require('./notifications/errors/validation');
|
|
||||||
const csv = require('csv-parser');
|
|
||||||
const stream = require('stream');
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = class PermissionsService {
|
|
||||||
static async create(data, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
try {
|
|
||||||
await PermissionsDBApi.create(
|
|
||||||
data,
|
|
||||||
{
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async bulkImport(req, res) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await processFile(req, res);
|
|
||||||
const bufferStream = new stream.PassThrough();
|
|
||||||
const results = [];
|
|
||||||
|
|
||||||
await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
bufferStream
|
|
||||||
.pipe(csv())
|
|
||||||
.on('data', (data) => results.push(data))
|
|
||||||
.on('end', async () => {
|
|
||||||
console.log('CSV results', results);
|
|
||||||
resolve();
|
|
||||||
})
|
|
||||||
.on('error', (error) => reject(error));
|
|
||||||
})
|
|
||||||
|
|
||||||
await PermissionsDBApi.bulkImport(results, {
|
|
||||||
transaction,
|
|
||||||
ignoreDuplicates: true,
|
|
||||||
validate: true,
|
|
||||||
currentUser: req.currentUser
|
|
||||||
});
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async update(data, id, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
try {
|
|
||||||
let permissions = await PermissionsDBApi.findBy(
|
|
||||||
{id},
|
|
||||||
{transaction},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!permissions) {
|
|
||||||
throw new ValidationError(
|
|
||||||
'permissionsNotFound',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedPermissions = await PermissionsDBApi.update(
|
|
||||||
id,
|
|
||||||
data,
|
|
||||||
{
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
return updatedPermissions;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async deleteByIds(ids, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await PermissionsDBApi.deleteByIds(ids, {
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
});
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async remove(id, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await PermissionsDBApi.remove(
|
|
||||||
id,
|
|
||||||
{
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = createEntityService(PermissionsDBApi, {
|
||||||
|
entityName: 'permissions',
|
||||||
|
});
|
||||||
|
|||||||
@ -1,136 +1,6 @@
|
|||||||
const db = require('../db/models');
|
|
||||||
const Presigned_url_requestsDBApi = require('../db/api/presigned_url_requests');
|
const Presigned_url_requestsDBApi = require('../db/api/presigned_url_requests');
|
||||||
const processFile = require("../middlewares/upload");
|
const { createEntityService } = require('../factories/service.factory');
|
||||||
const ValidationError = require('./notifications/errors/validation');
|
|
||||||
const csv = require('csv-parser');
|
|
||||||
const stream = require('stream');
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = class Presigned_url_requestsService {
|
|
||||||
static async create(data, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
try {
|
|
||||||
await Presigned_url_requestsDBApi.create(
|
|
||||||
data,
|
|
||||||
{
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async bulkImport(req, res) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await processFile(req, res);
|
|
||||||
const bufferStream = new stream.PassThrough();
|
|
||||||
const results = [];
|
|
||||||
|
|
||||||
await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
bufferStream
|
|
||||||
.pipe(csv())
|
|
||||||
.on('data', (data) => results.push(data))
|
|
||||||
.on('end', async () => {
|
|
||||||
console.log('CSV results', results);
|
|
||||||
resolve();
|
|
||||||
})
|
|
||||||
.on('error', (error) => reject(error));
|
|
||||||
})
|
|
||||||
|
|
||||||
await Presigned_url_requestsDBApi.bulkImport(results, {
|
|
||||||
transaction,
|
|
||||||
ignoreDuplicates: true,
|
|
||||||
validate: true,
|
|
||||||
currentUser: req.currentUser
|
|
||||||
});
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async update(data, id, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
try {
|
|
||||||
let presigned_url_requests = await Presigned_url_requestsDBApi.findBy(
|
|
||||||
{id},
|
|
||||||
{transaction},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!presigned_url_requests) {
|
|
||||||
throw new ValidationError(
|
|
||||||
'presigned_url_requestsNotFound',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedPresigned_url_requests = await Presigned_url_requestsDBApi.update(
|
|
||||||
id,
|
|
||||||
data,
|
|
||||||
{
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
return updatedPresigned_url_requests;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async deleteByIds(ids, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Presigned_url_requestsDBApi.deleteByIds(ids, {
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
});
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async remove(id, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Presigned_url_requestsDBApi.remove(
|
|
||||||
id,
|
|
||||||
{
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = createEntityService(Presigned_url_requestsDBApi, {
|
||||||
|
entityName: 'presigned_url_requests',
|
||||||
|
});
|
||||||
|
|||||||
@ -13,7 +13,7 @@ module.exports = class Project_audio_tracksService {
|
|||||||
static async create(data, currentUser) {
|
static async create(data, currentUser) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
try {
|
try {
|
||||||
await Project_audio_tracksDBApi.create(
|
const createdTrack = await Project_audio_tracksDBApi.create(
|
||||||
data,
|
data,
|
||||||
{
|
{
|
||||||
currentUser,
|
currentUser,
|
||||||
@ -22,6 +22,7 @@ module.exports = class Project_audio_tracksService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
|
return createdTrack;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@ -1,136 +1,6 @@
|
|||||||
const db = require('../db/models');
|
|
||||||
const Project_membershipsDBApi = require('../db/api/project_memberships');
|
const Project_membershipsDBApi = require('../db/api/project_memberships');
|
||||||
const processFile = require("../middlewares/upload");
|
const { createEntityService } = require('../factories/service.factory');
|
||||||
const ValidationError = require('./notifications/errors/validation');
|
|
||||||
const csv = require('csv-parser');
|
|
||||||
const stream = require('stream');
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = class Project_membershipsService {
|
|
||||||
static async create(data, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
try {
|
|
||||||
await Project_membershipsDBApi.create(
|
|
||||||
data,
|
|
||||||
{
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async bulkImport(req, res) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await processFile(req, res);
|
|
||||||
const bufferStream = new stream.PassThrough();
|
|
||||||
const results = [];
|
|
||||||
|
|
||||||
await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
bufferStream
|
|
||||||
.pipe(csv())
|
|
||||||
.on('data', (data) => results.push(data))
|
|
||||||
.on('end', async () => {
|
|
||||||
console.log('CSV results', results);
|
|
||||||
resolve();
|
|
||||||
})
|
|
||||||
.on('error', (error) => reject(error));
|
|
||||||
})
|
|
||||||
|
|
||||||
await Project_membershipsDBApi.bulkImport(results, {
|
|
||||||
transaction,
|
|
||||||
ignoreDuplicates: true,
|
|
||||||
validate: true,
|
|
||||||
currentUser: req.currentUser
|
|
||||||
});
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async update(data, id, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
try {
|
|
||||||
let project_memberships = await Project_membershipsDBApi.findBy(
|
|
||||||
{id},
|
|
||||||
{transaction},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!project_memberships) {
|
|
||||||
throw new ValidationError(
|
|
||||||
'project_membershipsNotFound',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedProject_memberships = await Project_membershipsDBApi.update(
|
|
||||||
id,
|
|
||||||
data,
|
|
||||||
{
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
return updatedProject_memberships;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async deleteByIds(ids, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Project_membershipsDBApi.deleteByIds(ids, {
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
});
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async remove(id, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Project_membershipsDBApi.remove(
|
|
||||||
id,
|
|
||||||
{
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = createEntityService(Project_membershipsDBApi, {
|
||||||
|
entityName: 'project_memberships',
|
||||||
|
});
|
||||||
|
|||||||
@ -106,8 +106,6 @@ module.exports = class ProjectsService {
|
|||||||
custom_css_json: sourceProject.custom_css_json,
|
custom_css_json: sourceProject.custom_css_json,
|
||||||
cdn_base_url: sourceProject.cdn_base_url,
|
cdn_base_url: sourceProject.cdn_base_url,
|
||||||
entry_page_slug: sourceProject.entry_page_slug,
|
entry_page_slug: sourceProject.entry_page_slug,
|
||||||
is_deleted: false,
|
|
||||||
deleted_at_time: null,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
currentUser,
|
currentUser,
|
||||||
@ -130,8 +128,6 @@ module.exports = class ProjectsService {
|
|||||||
duration_sec: sourceAsset.duration_sec,
|
duration_sec: sourceAsset.duration_sec,
|
||||||
checksum: sourceAsset.checksum,
|
checksum: sourceAsset.checksum,
|
||||||
is_public: sourceAsset.is_public,
|
is_public: sourceAsset.is_public,
|
||||||
is_deleted: false,
|
|
||||||
deleted_at_time: null,
|
|
||||||
projectId: clonedProject.id,
|
projectId: clonedProject.id,
|
||||||
createdById: currentUser.id,
|
createdById: currentUser.id,
|
||||||
updatedById: currentUser.id,
|
updatedById: currentUser.id,
|
||||||
|
|||||||
@ -1,136 +1,6 @@
|
|||||||
const db = require('../db/models');
|
|
||||||
const Publish_eventsDBApi = require('../db/api/publish_events');
|
const Publish_eventsDBApi = require('../db/api/publish_events');
|
||||||
const processFile = require("../middlewares/upload");
|
const { createEntityService } = require('../factories/service.factory');
|
||||||
const ValidationError = require('./notifications/errors/validation');
|
|
||||||
const csv = require('csv-parser');
|
|
||||||
const stream = require('stream');
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = class Publish_eventsService {
|
|
||||||
static async create(data, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
try {
|
|
||||||
await Publish_eventsDBApi.create(
|
|
||||||
data,
|
|
||||||
{
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async bulkImport(req, res) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await processFile(req, res);
|
|
||||||
const bufferStream = new stream.PassThrough();
|
|
||||||
const results = [];
|
|
||||||
|
|
||||||
await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
bufferStream
|
|
||||||
.pipe(csv())
|
|
||||||
.on('data', (data) => results.push(data))
|
|
||||||
.on('end', async () => {
|
|
||||||
console.log('CSV results', results);
|
|
||||||
resolve();
|
|
||||||
})
|
|
||||||
.on('error', (error) => reject(error));
|
|
||||||
})
|
|
||||||
|
|
||||||
await Publish_eventsDBApi.bulkImport(results, {
|
|
||||||
transaction,
|
|
||||||
ignoreDuplicates: true,
|
|
||||||
validate: true,
|
|
||||||
currentUser: req.currentUser
|
|
||||||
});
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async update(data, id, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
try {
|
|
||||||
let publish_events = await Publish_eventsDBApi.findBy(
|
|
||||||
{id},
|
|
||||||
{transaction},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!publish_events) {
|
|
||||||
throw new ValidationError(
|
|
||||||
'publish_eventsNotFound',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedPublish_events = await Publish_eventsDBApi.update(
|
|
||||||
id,
|
|
||||||
data,
|
|
||||||
{
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
return updatedPublish_events;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async deleteByIds(ids, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Publish_eventsDBApi.deleteByIds(ids, {
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
});
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async remove(id, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Publish_eventsDBApi.remove(
|
|
||||||
id,
|
|
||||||
{
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = createEntityService(Publish_eventsDBApi, {
|
||||||
|
entityName: 'publish_events',
|
||||||
|
});
|
||||||
|
|||||||
@ -1,136 +1,6 @@
|
|||||||
const db = require('../db/models');
|
|
||||||
const Pwa_cachesDBApi = require('../db/api/pwa_caches');
|
const Pwa_cachesDBApi = require('../db/api/pwa_caches');
|
||||||
const processFile = require("../middlewares/upload");
|
const { createEntityService } = require('../factories/service.factory');
|
||||||
const ValidationError = require('./notifications/errors/validation');
|
|
||||||
const csv = require('csv-parser');
|
|
||||||
const stream = require('stream');
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = class Pwa_cachesService {
|
|
||||||
static async create(data, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
try {
|
|
||||||
await Pwa_cachesDBApi.create(
|
|
||||||
data,
|
|
||||||
{
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async bulkImport(req, res) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await processFile(req, res);
|
|
||||||
const bufferStream = new stream.PassThrough();
|
|
||||||
const results = [];
|
|
||||||
|
|
||||||
await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
bufferStream
|
|
||||||
.pipe(csv())
|
|
||||||
.on('data', (data) => results.push(data))
|
|
||||||
.on('end', async () => {
|
|
||||||
console.log('CSV results', results);
|
|
||||||
resolve();
|
|
||||||
})
|
|
||||||
.on('error', (error) => reject(error));
|
|
||||||
})
|
|
||||||
|
|
||||||
await Pwa_cachesDBApi.bulkImport(results, {
|
|
||||||
transaction,
|
|
||||||
ignoreDuplicates: true,
|
|
||||||
validate: true,
|
|
||||||
currentUser: req.currentUser
|
|
||||||
});
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async update(data, id, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
try {
|
|
||||||
let pwa_caches = await Pwa_cachesDBApi.findBy(
|
|
||||||
{id},
|
|
||||||
{transaction},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!pwa_caches) {
|
|
||||||
throw new ValidationError(
|
|
||||||
'pwa_cachesNotFound',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedPwa_caches = await Pwa_cachesDBApi.update(
|
|
||||||
id,
|
|
||||||
data,
|
|
||||||
{
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
return updatedPwa_caches;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async deleteByIds(ids, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Pwa_cachesDBApi.deleteByIds(ids, {
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
});
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async remove(id, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Pwa_cachesDBApi.remove(
|
|
||||||
id,
|
|
||||||
{
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = createEntityService(Pwa_cachesDBApi, {
|
||||||
|
entityName: 'pwa_caches',
|
||||||
|
});
|
||||||
|
|||||||
@ -59,7 +59,7 @@ module.exports = class RolesService {
|
|||||||
static async create(data, currentUser) {
|
static async create(data, currentUser) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
try {
|
try {
|
||||||
await RolesDBApi.create(
|
const createdRole = await RolesDBApi.create(
|
||||||
data,
|
data,
|
||||||
{
|
{
|
||||||
currentUser,
|
currentUser,
|
||||||
@ -68,6 +68,7 @@ module.exports = class RolesService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
|
return createdRole;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@ -1,136 +1,6 @@
|
|||||||
const db = require('../db/models');
|
|
||||||
const Tour_pagesDBApi = require('../db/api/tour_pages');
|
const Tour_pagesDBApi = require('../db/api/tour_pages');
|
||||||
const processFile = require("../middlewares/upload");
|
const { createEntityService } = require('../factories/service.factory');
|
||||||
const ValidationError = require('./notifications/errors/validation');
|
|
||||||
const csv = require('csv-parser');
|
|
||||||
const stream = require('stream');
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = class Tour_pagesService {
|
|
||||||
static async create(data, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
try {
|
|
||||||
await Tour_pagesDBApi.create(
|
|
||||||
data,
|
|
||||||
{
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async bulkImport(req, res) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await processFile(req, res);
|
|
||||||
const bufferStream = new stream.PassThrough();
|
|
||||||
const results = [];
|
|
||||||
|
|
||||||
await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
bufferStream
|
|
||||||
.pipe(csv())
|
|
||||||
.on('data', (data) => results.push(data))
|
|
||||||
.on('end', async () => {
|
|
||||||
console.log('CSV results', results);
|
|
||||||
resolve();
|
|
||||||
})
|
|
||||||
.on('error', (error) => reject(error));
|
|
||||||
})
|
|
||||||
|
|
||||||
await Tour_pagesDBApi.bulkImport(results, {
|
|
||||||
transaction,
|
|
||||||
ignoreDuplicates: true,
|
|
||||||
validate: true,
|
|
||||||
currentUser: req.currentUser
|
|
||||||
});
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async update(data, id, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
try {
|
|
||||||
let tour_pages = await Tour_pagesDBApi.findBy(
|
|
||||||
{id},
|
|
||||||
{transaction},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!tour_pages) {
|
|
||||||
throw new ValidationError(
|
|
||||||
'tour_pagesNotFound',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedTour_pages = await Tour_pagesDBApi.update(
|
|
||||||
id,
|
|
||||||
data,
|
|
||||||
{
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
return updatedTour_pages;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async deleteByIds(ids, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Tour_pagesDBApi.deleteByIds(ids, {
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
});
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async remove(id, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Tour_pagesDBApi.remove(
|
|
||||||
id,
|
|
||||||
{
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = createEntityService(Tour_pagesDBApi, {
|
||||||
|
entityName: 'tour_pages',
|
||||||
|
});
|
||||||
|
|||||||
@ -1,136 +1,6 @@
|
|||||||
const db = require('../db/models');
|
|
||||||
const TransitionsDBApi = require('../db/api/transitions');
|
const TransitionsDBApi = require('../db/api/transitions');
|
||||||
const processFile = require("../middlewares/upload");
|
const { createEntityService } = require('../factories/service.factory');
|
||||||
const ValidationError = require('./notifications/errors/validation');
|
|
||||||
const csv = require('csv-parser');
|
|
||||||
const stream = require('stream');
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = class TransitionsService {
|
|
||||||
static async create(data, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
try {
|
|
||||||
await TransitionsDBApi.create(
|
|
||||||
data,
|
|
||||||
{
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async bulkImport(req, res) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await processFile(req, res);
|
|
||||||
const bufferStream = new stream.PassThrough();
|
|
||||||
const results = [];
|
|
||||||
|
|
||||||
await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
bufferStream
|
|
||||||
.pipe(csv())
|
|
||||||
.on('data', (data) => results.push(data))
|
|
||||||
.on('end', async () => {
|
|
||||||
console.log('CSV results', results);
|
|
||||||
resolve();
|
|
||||||
})
|
|
||||||
.on('error', (error) => reject(error));
|
|
||||||
})
|
|
||||||
|
|
||||||
await TransitionsDBApi.bulkImport(results, {
|
|
||||||
transaction,
|
|
||||||
ignoreDuplicates: true,
|
|
||||||
validate: true,
|
|
||||||
currentUser: req.currentUser
|
|
||||||
});
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async update(data, id, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
try {
|
|
||||||
let transitions = await TransitionsDBApi.findBy(
|
|
||||||
{id},
|
|
||||||
{transaction},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!transitions) {
|
|
||||||
throw new ValidationError(
|
|
||||||
'transitionsNotFound',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedTransitions = await TransitionsDBApi.update(
|
|
||||||
id,
|
|
||||||
data,
|
|
||||||
{
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
return updatedTransitions;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async deleteByIds(ids, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await TransitionsDBApi.deleteByIds(ids, {
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
});
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async remove(id, currentUser) {
|
|
||||||
const transaction = await db.sequelize.transaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await TransitionsDBApi.remove(
|
|
||||||
id,
|
|
||||||
{
|
|
||||||
currentUser,
|
|
||||||
transaction,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (error) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = createEntityService(TransitionsDBApi, {
|
||||||
|
entityName: 'transitions',
|
||||||
|
});
|
||||||
|
|||||||
@ -2,11 +2,7 @@ import React from 'react';
|
|||||||
import BaseIcon from '../BaseIcon';
|
import BaseIcon from '../BaseIcon';
|
||||||
import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js';
|
import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import {
|
import { GridActionsCellItem, GridRowParams } from '@mui/x-data-grid';
|
||||||
GridActionsCellItem,
|
|
||||||
GridRowParams,
|
|
||||||
GridValueGetterParams,
|
|
||||||
} from '@mui/x-data-grid';
|
|
||||||
import ImageField from '../ImageField';
|
import ImageField from '../ImageField';
|
||||||
import { saveFile } from '../../helpers/fileSaver';
|
import { saveFile } from '../../helpers/fileSaver';
|
||||||
import dataFormatter from '../../helpers/dataFormatter';
|
import dataFormatter from '../../helpers/dataFormatter';
|
||||||
@ -54,8 +50,7 @@ export const loadColumns = async (
|
|||||||
getOptionValue: (value: any) => value?.id,
|
getOptionValue: (value: any) => value?.id,
|
||||||
getOptionLabel: (value: any) => value?.label,
|
getOptionLabel: (value: any) => value?.label,
|
||||||
valueOptions: await callOptionsApi('projects'),
|
valueOptions: await callOptionsApi('projects'),
|
||||||
valueGetter: (params: GridValueGetterParams) =>
|
valueGetter: (value) => value?.id ?? value,
|
||||||
params?.value?.id ?? params?.value,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -86,8 +81,7 @@ export const loadColumns = async (
|
|||||||
getOptionValue: (value: any) => value?.id,
|
getOptionValue: (value: any) => value?.id,
|
||||||
getOptionLabel: (value: any) => value?.label,
|
getOptionLabel: (value: any) => value?.label,
|
||||||
valueOptions: await callOptionsApi('users'),
|
valueOptions: await callOptionsApi('users'),
|
||||||
valueGetter: (params: GridValueGetterParams) =>
|
valueGetter: (value) => value?.id ?? value,
|
||||||
params?.value?.id ?? params?.value,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -138,8 +132,7 @@ export const loadColumns = async (
|
|||||||
editable: hasUpdatePermission,
|
editable: hasUpdatePermission,
|
||||||
|
|
||||||
type: 'dateTime',
|
type: 'dateTime',
|
||||||
valueGetter: (params: GridValueGetterParams) =>
|
valueGetter: (_value, row) => new Date(row.accessed_at),
|
||||||
new Date(params.row.accessed_at),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@ -2,11 +2,7 @@ import React from 'react';
|
|||||||
import BaseIcon from '../BaseIcon';
|
import BaseIcon from '../BaseIcon';
|
||||||
import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js';
|
import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import {
|
import { GridActionsCellItem, GridRowParams } from '@mui/x-data-grid';
|
||||||
GridActionsCellItem,
|
|
||||||
GridRowParams,
|
|
||||||
GridValueGetterParams,
|
|
||||||
} from '@mui/x-data-grid';
|
|
||||||
import ImageField from '../ImageField';
|
import ImageField from '../ImageField';
|
||||||
import { saveFile } from '../../helpers/fileSaver';
|
import { saveFile } from '../../helpers/fileSaver';
|
||||||
import dataFormatter from '../../helpers/dataFormatter';
|
import dataFormatter from '../../helpers/dataFormatter';
|
||||||
@ -54,8 +50,7 @@ export const loadColumns = async (
|
|||||||
getOptionValue: (value: any) => value?.id,
|
getOptionValue: (value: any) => value?.id,
|
||||||
getOptionLabel: (value: any) => value?.label,
|
getOptionLabel: (value: any) => value?.label,
|
||||||
valueOptions: await callOptionsApi('assets'),
|
valueOptions: await callOptionsApi('assets'),
|
||||||
valueGetter: (params: GridValueGetterParams) =>
|
valueGetter: (value) => value?.id ?? value,
|
||||||
params?.value?.id ?? params?.value,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
154
frontend/src/components/Assets/AssetSectionCard.tsx
Normal file
154
frontend/src/components/Assets/AssetSectionCard.tsx
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import BaseButton from '../BaseButton';
|
||||||
|
import CardBox from '../CardBox';
|
||||||
|
import UploadProgressList, { UploadQueueItem } from './UploadProgressList';
|
||||||
|
|
||||||
|
export type Asset = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
asset_type: 'image' | 'video' | 'audio' | 'file';
|
||||||
|
type?:
|
||||||
|
| 'icon'
|
||||||
|
| 'background_image'
|
||||||
|
| 'audio'
|
||||||
|
| 'video'
|
||||||
|
| 'transition'
|
||||||
|
| 'logo'
|
||||||
|
| 'favicon'
|
||||||
|
| 'document'
|
||||||
|
| 'general';
|
||||||
|
cdn_url?: string | null;
|
||||||
|
mime_type?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AssetSection = {
|
||||||
|
key:
|
||||||
|
| 'images'
|
||||||
|
| 'backgroundImages'
|
||||||
|
| 'audio'
|
||||||
|
| 'video'
|
||||||
|
| 'transitions'
|
||||||
|
| 'logo';
|
||||||
|
label: string;
|
||||||
|
accept: string;
|
||||||
|
assetFormat: 'image' | 'video' | 'audio';
|
||||||
|
assetCategory: NonNullable<Asset['type']>;
|
||||||
|
legacyTag: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AssetSectionCardProps = {
|
||||||
|
section: AssetSection;
|
||||||
|
assets: Asset[];
|
||||||
|
uploadQueue: UploadQueueItem[];
|
||||||
|
isUploading: boolean;
|
||||||
|
isLoadingAssets: boolean;
|
||||||
|
hasCreatePermission: boolean;
|
||||||
|
hasDeletePermission: boolean;
|
||||||
|
deletingAssetId: string;
|
||||||
|
onUpload: (files: File[]) => void;
|
||||||
|
onDeleteAsset: (assetId: string) => void;
|
||||||
|
disabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AssetSectionCard: React.FC<AssetSectionCardProps> = ({
|
||||||
|
section,
|
||||||
|
assets,
|
||||||
|
uploadQueue,
|
||||||
|
isUploading,
|
||||||
|
isLoadingAssets,
|
||||||
|
hasCreatePermission,
|
||||||
|
hasDeletePermission,
|
||||||
|
deletingAssetId,
|
||||||
|
onUpload,
|
||||||
|
onDeleteAsset,
|
||||||
|
disabled,
|
||||||
|
}) => {
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
|
||||||
|
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const files = Array.from(event.target.files || []);
|
||||||
|
onUpload(files);
|
||||||
|
event.currentTarget.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardBox className='h-full'>
|
||||||
|
<div className='flex items-center justify-between gap-3 mb-4'>
|
||||||
|
<h3 className='text-lg font-semibold'>{section.label}</h3>
|
||||||
|
{!hasCreatePermission && (
|
||||||
|
<p className='text-xs text-gray-500'>
|
||||||
|
You do not have upload permission
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type='file'
|
||||||
|
multiple
|
||||||
|
accept={section.accept}
|
||||||
|
className='w-full border border-gray-300 rounded px-2 py-2 mb-3 bg-white dark:bg-dark-800'
|
||||||
|
disabled={isUploading || disabled || !hasCreatePermission}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isUploading && (
|
||||||
|
<p className='text-sm text-gray-500 mb-3'>
|
||||||
|
Uploading {section.label.toLowerCase()} in chunks...
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<UploadProgressList items={uploadQueue} />
|
||||||
|
|
||||||
|
{isLoadingAssets && (
|
||||||
|
<p className='text-sm text-gray-500'>Loading assets...</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoadingAssets && assets.length > 0 && (
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='mb-2 text-xs underline'
|
||||||
|
onClick={() => setIsExpanded((prev) => !prev)}
|
||||||
|
>
|
||||||
|
{isExpanded
|
||||||
|
? `Hide uploaded ${section.label.toLowerCase()}`
|
||||||
|
: `Show uploaded ${section.label.toLowerCase()} (${assets.length})`}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoadingAssets && assets.length === 0 && (
|
||||||
|
<p className='text-sm text-gray-500'>
|
||||||
|
No uploaded {section.label.toLowerCase()}.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoadingAssets && assets.length > 0 && isExpanded && (
|
||||||
|
<ul className='space-y-2'>
|
||||||
|
{assets.map((asset) => (
|
||||||
|
<li
|
||||||
|
key={asset.id}
|
||||||
|
className='flex items-center justify-between gap-2 p-2 border border-gray-200 dark:border-dark-700 rounded'
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={asset.cdn_url || '#'}
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
className='text-sm underline truncate'
|
||||||
|
>
|
||||||
|
{asset.name.replace(/^\[[^\]]+\]\s*/, '')}
|
||||||
|
</a>
|
||||||
|
<BaseButton
|
||||||
|
color='danger'
|
||||||
|
label='X'
|
||||||
|
small
|
||||||
|
disabled={deletingAssetId === asset.id || !hasDeletePermission}
|
||||||
|
onClick={() => onDeleteAsset(asset.id)}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</CardBox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AssetSectionCard;
|
||||||
114
frontend/src/components/Assets/ProjectSelector.tsx
Normal file
114
frontend/src/components/Assets/ProjectSelector.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
export type Project = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface UseProjectSelectorOptions {
|
||||||
|
currentUser: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseProjectSelectorReturn {
|
||||||
|
projects: Project[];
|
||||||
|
selectedProjectId: string;
|
||||||
|
isLoadingProjects: boolean;
|
||||||
|
selectedProjectName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useProjectSelector({
|
||||||
|
currentUser,
|
||||||
|
}: UseProjectSelectorOptions): UseProjectSelectorReturn {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const routeProjectId = useMemo(() => {
|
||||||
|
const value = router.query.projectId;
|
||||||
|
if (Array.isArray(value)) return value[0] || '';
|
||||||
|
return String(value || '');
|
||||||
|
}, [router.query.projectId]);
|
||||||
|
|
||||||
|
const [projects, setProjects] = useState<Project[]>([]);
|
||||||
|
const [selectedProjectId, setSelectedProjectId] = useState('');
|
||||||
|
const [isLoadingProjects, setIsLoadingProjects] = useState(false);
|
||||||
|
|
||||||
|
const loadProjects = useCallback(async () => {
|
||||||
|
setIsLoadingProjects(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
let rows: Project[] = [];
|
||||||
|
const response = await axios.get(
|
||||||
|
'/projects?limit=100&page=0&sort=desc&field=updatedAt',
|
||||||
|
);
|
||||||
|
rows = Array.isArray(response?.data?.rows) ? response.data.rows : [];
|
||||||
|
|
||||||
|
if (rows.length === 0) {
|
||||||
|
const autocompleteResponse = await axios.get(
|
||||||
|
'/projects/autocomplete?limit=100',
|
||||||
|
);
|
||||||
|
const autocompleteItems = Array.isArray(autocompleteResponse?.data)
|
||||||
|
? autocompleteResponse.data
|
||||||
|
: [];
|
||||||
|
rows = autocompleteItems.map((item: { id: string; label: string }) => ({
|
||||||
|
id: item.id,
|
||||||
|
name: item.label,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rows.length === 0) {
|
||||||
|
toast('Please create a project first', {
|
||||||
|
type: 'info',
|
||||||
|
position: 'bottom-center',
|
||||||
|
});
|
||||||
|
router.replace('/projects/projects-new');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setProjects(rows);
|
||||||
|
|
||||||
|
if (
|
||||||
|
routeProjectId &&
|
||||||
|
rows.some((project) => project.id === routeProjectId)
|
||||||
|
) {
|
||||||
|
setSelectedProjectId(routeProjectId);
|
||||||
|
} else {
|
||||||
|
setSelectedProjectId((prev) => {
|
||||||
|
if (rows.some((project) => project.id === prev)) return prev;
|
||||||
|
return rows[0]?.id || '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : String(error);
|
||||||
|
console.error('Failed to load projects:', errorMessage);
|
||||||
|
setProjects([]);
|
||||||
|
setSelectedProjectId('');
|
||||||
|
toast('Failed to load projects', {
|
||||||
|
type: 'error',
|
||||||
|
position: 'bottom-center',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoadingProjects(false);
|
||||||
|
}
|
||||||
|
}, [routeProjectId, router]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!currentUser) return;
|
||||||
|
loadProjects();
|
||||||
|
}, [routeProjectId, currentUser, loadProjects]);
|
||||||
|
|
||||||
|
const selectedProjectName = useMemo(() => {
|
||||||
|
return projects.find((p) => p.id === selectedProjectId)?.name || '';
|
||||||
|
}, [projects, selectedProjectId]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
projects,
|
||||||
|
selectedProjectId,
|
||||||
|
isLoadingProjects,
|
||||||
|
selectedProjectName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useProjectSelector;
|
||||||
56
frontend/src/components/Assets/UploadProgressList.tsx
Normal file
56
frontend/src/components/Assets/UploadProgressList.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export type UploadQueueItem = {
|
||||||
|
id: string;
|
||||||
|
fileName: string;
|
||||||
|
progress: number;
|
||||||
|
status: 'queued' | 'uploading' | 'saving' | 'success' | 'error';
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type UploadProgressListProps = {
|
||||||
|
items: UploadQueueItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const UploadProgressList: React.FC<UploadProgressListProps> = ({ items }) => {
|
||||||
|
const pendingItems = items.filter((item) => item.status !== 'success');
|
||||||
|
|
||||||
|
if (pendingItems.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className='space-y-1 mb-3 max-h-40 overflow-auto'>
|
||||||
|
{pendingItems.map((item) => (
|
||||||
|
<li
|
||||||
|
key={item.id}
|
||||||
|
className='text-xs border border-gray-200 dark:border-dark-700 rounded p-2'
|
||||||
|
>
|
||||||
|
<div className='flex items-center justify-between gap-2'>
|
||||||
|
<span className='truncate'>{item.fileName}</span>
|
||||||
|
<span>
|
||||||
|
{item.status === 'error'
|
||||||
|
? 'Error'
|
||||||
|
: item.status === 'success'
|
||||||
|
? 'Done'
|
||||||
|
: item.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{item.status !== 'success' && (
|
||||||
|
<div className='w-full h-1 bg-gray-200 rounded mt-1'>
|
||||||
|
<div
|
||||||
|
className='h-1 bg-blue-500 rounded'
|
||||||
|
style={{ width: `${item.progress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{item.error && (
|
||||||
|
<p className='text-red-500 mt-1 truncate'>{item.error}</p>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UploadProgressList;
|
||||||
@ -2,11 +2,7 @@ import React from 'react';
|
|||||||
import BaseIcon from '../BaseIcon';
|
import BaseIcon from '../BaseIcon';
|
||||||
import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js';
|
import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import {
|
import { GridActionsCellItem, GridRowParams } from '@mui/x-data-grid';
|
||||||
GridActionsCellItem,
|
|
||||||
GridRowParams,
|
|
||||||
GridValueGetterParams,
|
|
||||||
} from '@mui/x-data-grid';
|
|
||||||
import ImageField from '../ImageField';
|
import ImageField from '../ImageField';
|
||||||
import { saveFile } from '../../helpers/fileSaver';
|
import { saveFile } from '../../helpers/fileSaver';
|
||||||
import dataFormatter from '../../helpers/dataFormatter';
|
import dataFormatter from '../../helpers/dataFormatter';
|
||||||
@ -54,8 +50,7 @@ export const loadColumns = async (
|
|||||||
getOptionValue: (value: any) => value?.id,
|
getOptionValue: (value: any) => value?.id,
|
||||||
getOptionLabel: (value: any) => value?.label,
|
getOptionLabel: (value: any) => value?.label,
|
||||||
valueOptions: await callOptionsApi('projects'),
|
valueOptions: await callOptionsApi('projects'),
|
||||||
valueGetter: (params: GridValueGetterParams) =>
|
valueGetter: (value) => value?.id ?? value,
|
||||||
params?.value?.id ?? params?.value,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -238,8 +233,7 @@ export const loadColumns = async (
|
|||||||
editable: hasUpdatePermission,
|
editable: hasUpdatePermission,
|
||||||
|
|
||||||
type: 'dateTime',
|
type: 'dateTime',
|
||||||
valueGetter: (params: GridValueGetterParams) =>
|
valueGetter: (_value, row) => new Date(row.deleted_at_time),
|
||||||
new Date(params.row.deleted_at_time),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
244
frontend/src/components/Assets/useAssetUploader.ts
Normal file
244
frontend/src/components/Assets/useAssetUploader.ts
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import FileUploader from '../Uploaders/UploadService';
|
||||||
|
import type { AssetSection } from './AssetSectionCard';
|
||||||
|
import type { UploadQueueItem } from './UploadProgressList';
|
||||||
|
|
||||||
|
interface UseAssetUploaderOptions {
|
||||||
|
selectedProjectId: string;
|
||||||
|
onUploadComplete: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseAssetUploaderReturn {
|
||||||
|
uploadingSections: string[];
|
||||||
|
uploadQueues: Record<string, UploadQueueItem[]>;
|
||||||
|
runBatchUpload: (section: AssetSection, files: File[]) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAssetUploader({
|
||||||
|
selectedProjectId,
|
||||||
|
onUploadComplete,
|
||||||
|
}: UseAssetUploaderOptions): UseAssetUploaderReturn {
|
||||||
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
|
const [uploadingSections, setUploadingSections] = useState<string[]>([]);
|
||||||
|
const [uploadQueues, setUploadQueues] = useState<
|
||||||
|
Record<string, UploadQueueItem[]>
|
||||||
|
>({});
|
||||||
|
|
||||||
|
// Cleanup on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (abortControllerRef.current) {
|
||||||
|
abortControllerRef.current.abort();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const addSectionUpload = useCallback(
|
||||||
|
(sectionKey: string, items: UploadQueueItem[]) => {
|
||||||
|
setUploadQueues((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[sectionKey]: [...(prev[sectionKey] || []), ...items],
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateSectionUpload = useCallback(
|
||||||
|
(sectionKey: string, itemId: string, patch: Partial<UploadQueueItem>) => {
|
||||||
|
setUploadQueues((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[sectionKey]: (prev[sectionKey] || []).map((item) =>
|
||||||
|
item.id === itemId ? { ...item, ...patch } : item,
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const markSectionUploading = useCallback(
|
||||||
|
(sectionKey: string, isUploading: boolean) => {
|
||||||
|
setUploadingSections((prev) => {
|
||||||
|
if (isUploading) {
|
||||||
|
if (prev.includes(sectionKey)) return prev;
|
||||||
|
return [...prev, sectionKey];
|
||||||
|
}
|
||||||
|
return prev.filter((key) => key !== sectionKey);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const uploadAssetFile = useCallback(
|
||||||
|
async (
|
||||||
|
section: AssetSection,
|
||||||
|
projectId: string,
|
||||||
|
itemId: string,
|
||||||
|
file: File,
|
||||||
|
signal?: AbortSignal,
|
||||||
|
) => {
|
||||||
|
updateSectionUpload(section.key, itemId, {
|
||||||
|
status: 'uploading',
|
||||||
|
progress: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const remoteFile = await FileUploader.uploadChunked(
|
||||||
|
`assets/${projectId}`,
|
||||||
|
file,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
chunkSize: 5 * 1024 * 1024,
|
||||||
|
maxRetries: 3,
|
||||||
|
signal,
|
||||||
|
onProgress: (progress: number) => {
|
||||||
|
updateSectionUpload(section.key, itemId, {
|
||||||
|
progress,
|
||||||
|
status: 'uploading',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onStatus: (status: string) => {
|
||||||
|
if (status === 'finalizing') {
|
||||||
|
updateSectionUpload(section.key, itemId, { status: 'saving' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await axios.post('/assets', {
|
||||||
|
data: {
|
||||||
|
project: projectId,
|
||||||
|
name: file.name,
|
||||||
|
asset_type: section.assetFormat,
|
||||||
|
type: section.assetCategory,
|
||||||
|
cdn_url: remoteFile.publicUrl,
|
||||||
|
storage_key: remoteFile.privateUrl,
|
||||||
|
mime_type: file.type || null,
|
||||||
|
size_mb: Number((file.size / (1024 * 1024)).toFixed(4)),
|
||||||
|
is_public: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
updateSectionUpload(section.key, itemId, {
|
||||||
|
status: 'success',
|
||||||
|
progress: 100,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[updateSectionUpload],
|
||||||
|
);
|
||||||
|
|
||||||
|
const runBatchUpload = useCallback(
|
||||||
|
async (section: AssetSection, files: File[]) => {
|
||||||
|
if (!selectedProjectId) {
|
||||||
|
toast('Select a project first', {
|
||||||
|
type: 'warning',
|
||||||
|
position: 'bottom-center',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!files.length) return;
|
||||||
|
|
||||||
|
if (abortControllerRef.current) {
|
||||||
|
abortControllerRef.current.abort();
|
||||||
|
}
|
||||||
|
abortControllerRef.current = new AbortController();
|
||||||
|
const { signal } = abortControllerRef.current;
|
||||||
|
|
||||||
|
const queueItems: UploadQueueItem[] = files.map((file) => ({
|
||||||
|
id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
||||||
|
fileName: file.name,
|
||||||
|
progress: 0,
|
||||||
|
status: 'queued',
|
||||||
|
}));
|
||||||
|
|
||||||
|
addSectionUpload(section.key, queueItems);
|
||||||
|
markSectionUploading(section.key, true);
|
||||||
|
|
||||||
|
const queue = files.map((file, index) => ({
|
||||||
|
file,
|
||||||
|
itemId: queueItems[index].id,
|
||||||
|
}));
|
||||||
|
const maxConcurrent = 2;
|
||||||
|
let currentIndex = 0;
|
||||||
|
let failedCount = 0;
|
||||||
|
|
||||||
|
const worker = async () => {
|
||||||
|
while (currentIndex < queue.length) {
|
||||||
|
if (signal.aborted) break;
|
||||||
|
|
||||||
|
const nextIndex = currentIndex;
|
||||||
|
currentIndex += 1;
|
||||||
|
const item = queue[nextIndex];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await uploadAssetFile(
|
||||||
|
section,
|
||||||
|
selectedProjectId,
|
||||||
|
item.itemId,
|
||||||
|
item.file,
|
||||||
|
signal,
|
||||||
|
);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (signal.aborted) break;
|
||||||
|
|
||||||
|
const axiosError = error as {
|
||||||
|
response?: { data?: { message?: string } };
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
const message =
|
||||||
|
axiosError?.response?.data?.message ||
|
||||||
|
axiosError?.message ||
|
||||||
|
'Upload failed';
|
||||||
|
console.error(`Failed to upload ${item.file.name}:`, error);
|
||||||
|
failedCount += 1;
|
||||||
|
updateSectionUpload(section.key, item.itemId, {
|
||||||
|
status: 'error',
|
||||||
|
error: message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all(
|
||||||
|
Array.from({ length: Math.min(maxConcurrent, queue.length) }, () =>
|
||||||
|
worker(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (!signal.aborted) {
|
||||||
|
onUploadComplete();
|
||||||
|
if (failedCount > 0) {
|
||||||
|
toast(
|
||||||
|
`Batch upload finished: ${queue.length - failedCount}/${queue.length} succeeded`,
|
||||||
|
{ type: 'warning', position: 'bottom-center' },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
toast(`Batch upload finished for ${section.label}`, {
|
||||||
|
type: 'success',
|
||||||
|
position: 'bottom-center',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
markSectionUploading(section.key, false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
selectedProjectId,
|
||||||
|
addSectionUpload,
|
||||||
|
markSectionUploading,
|
||||||
|
uploadAssetFile,
|
||||||
|
updateSectionUpload,
|
||||||
|
onUploadComplete,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
uploadingSections,
|
||||||
|
uploadQueues,
|
||||||
|
runBatchUpload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useAssetUploader;
|
||||||
@ -1,533 +0,0 @@
|
|||||||
/**
|
|
||||||
* GenericTable Component
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { useEffect, useState, useMemo, useCallback } from 'react';
|
|
||||||
import { createPortal } from 'react-dom';
|
|
||||||
import { ToastContainer, toast } from 'react-toastify';
|
|
||||||
import {
|
|
||||||
DataGrid,
|
|
||||||
GridColDef,
|
|
||||||
GridSortModel,
|
|
||||||
GridRowSelectionModel,
|
|
||||||
} from '@mui/x-data-grid';
|
|
||||||
import { Field, Form, Formik } from 'formik';
|
|
||||||
import BaseButton from '../BaseButton';
|
|
||||||
import CardBoxModal from '../CardBoxModal';
|
|
||||||
import CardBox from '../CardBox';
|
|
||||||
import { useAppDispatch, useAppSelector } from '../../stores/hooks';
|
|
||||||
import type { RootState } from '../../stores/store';
|
|
||||||
import type { AsyncThunk } from '@reduxjs/toolkit';
|
|
||||||
import type { BaseEntity } from '../../types/entities';
|
|
||||||
import type { FetchParams } from '../../types/api';
|
|
||||||
import type { NotificationState } from '../../types/redux';
|
|
||||||
import type { Filter, FilterItem, FilterFields } from '../../types/filters';
|
|
||||||
import dataFormatter from '../../helpers/dataFormatter';
|
|
||||||
import { dataGridStyles } from '../../styles';
|
|
||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
// Entity slice state interface
|
|
||||||
interface EntitySliceState<T> {
|
|
||||||
loading: boolean;
|
|
||||||
count: number;
|
|
||||||
refetch: boolean;
|
|
||||||
notify: NotificationState;
|
|
||||||
[entityName: string]: T[] | boolean | number | NotificationState | unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Props for GenericTable
|
|
||||||
interface GenericTableProps<T extends BaseEntity> {
|
|
||||||
entityName: string;
|
|
||||||
sliceSelector: (state: RootState) => EntitySliceState<T>;
|
|
||||||
fetchAction: AsyncThunk<unknown, FetchParams, { rejectValue: unknown }>;
|
|
||||||
updateAction?: AsyncThunk<
|
|
||||||
unknown,
|
|
||||||
{ id: string; data: Partial<T> },
|
|
||||||
{ rejectValue: unknown }
|
|
||||||
>;
|
|
||||||
deleteAction: AsyncThunk<unknown, string, { rejectValue: unknown }>;
|
|
||||||
deleteByIdsAction?: AsyncThunk<unknown, string[], { rejectValue: unknown }>;
|
|
||||||
setRefetchAction: (value: boolean) => { type: string; payload: boolean };
|
|
||||||
loadColumnsFunction: (
|
|
||||||
handleDelete: (id: string) => void,
|
|
||||||
entityPath: string,
|
|
||||||
currentUser: unknown,
|
|
||||||
) => Promise<GridColDef[]>;
|
|
||||||
filters: Filter[];
|
|
||||||
filterItems: FilterItem[];
|
|
||||||
setFilterItems: (items: FilterItem[]) => void;
|
|
||||||
perPage?: number;
|
|
||||||
showGrid?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const GenericTable = <T extends BaseEntity>({
|
|
||||||
entityName,
|
|
||||||
sliceSelector,
|
|
||||||
fetchAction,
|
|
||||||
updateAction,
|
|
||||||
deleteAction,
|
|
||||||
deleteByIdsAction,
|
|
||||||
setRefetchAction,
|
|
||||||
loadColumnsFunction,
|
|
||||||
filters,
|
|
||||||
filterItems,
|
|
||||||
setFilterItems,
|
|
||||||
perPage = 10,
|
|
||||||
}: GenericTableProps<T>) => {
|
|
||||||
const notify = (
|
|
||||||
type: 'success' | 'error' | 'info' | 'warning',
|
|
||||||
msg: string,
|
|
||||||
) => toast(msg, { type, position: 'bottom-center' });
|
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const entityState = useAppSelector(sliceSelector);
|
|
||||||
const { currentUser } = useAppSelector((state) => state.auth);
|
|
||||||
const focusRing = useAppSelector((state) => state.style.focusRingColor);
|
|
||||||
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
|
|
||||||
const corners = useAppSelector((state) => state.style.corners);
|
|
||||||
|
|
||||||
// Extract data from state
|
|
||||||
const data = (entityState[entityName] as T[]) || [];
|
|
||||||
const { loading, count, refetch, notify: entityNotify } = entityState;
|
|
||||||
|
|
||||||
// Local state
|
|
||||||
const [currentPage, setCurrentPage] = useState(0);
|
|
||||||
const [columns, setColumns] = useState<GridColDef[]>([]);
|
|
||||||
const [filterRequest, setFilterRequest] = useState('');
|
|
||||||
const [selectedRows, setSelectedRows] = useState<GridRowSelectionModel>([]);
|
|
||||||
const [sortModel, setSortModel] = useState<GridSortModel>([
|
|
||||||
{ field: '', sort: 'desc' },
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Delete modal state
|
|
||||||
const [id, setId] = useState<string | null>(null);
|
|
||||||
const [isModalTrashActive, setIsModalTrashActive] = useState(false);
|
|
||||||
|
|
||||||
// Calculate number of pages
|
|
||||||
const numPages = useMemo(() => {
|
|
||||||
return count === 0 ? 1 : Math.ceil(count / perPage);
|
|
||||||
}, [count, perPage]);
|
|
||||||
|
|
||||||
// Load data function
|
|
||||||
const loadData = useCallback(
|
|
||||||
async (page = currentPage, request = filterRequest) => {
|
|
||||||
if (page !== currentPage) setCurrentPage(page);
|
|
||||||
if (request !== filterRequest) setFilterRequest(request);
|
|
||||||
|
|
||||||
const { sort, field } = sortModel[0] || { sort: 'desc', field: '' };
|
|
||||||
const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`;
|
|
||||||
|
|
||||||
dispatch(fetchAction({ query }));
|
|
||||||
},
|
|
||||||
[currentPage, filterRequest, sortModel, perPage, dispatch, fetchAction],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Show notifications
|
|
||||||
useEffect(() => {
|
|
||||||
if (entityNotify.showNotification) {
|
|
||||||
notify(
|
|
||||||
entityNotify.typeNotification as
|
|
||||||
| 'success'
|
|
||||||
| 'error'
|
|
||||||
| 'info'
|
|
||||||
| 'warning',
|
|
||||||
entityNotify.textNotification,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [entityNotify.showNotification]);
|
|
||||||
|
|
||||||
// Load data on sort model or user change
|
|
||||||
useEffect(() => {
|
|
||||||
if (!currentUser) return;
|
|
||||||
loadData();
|
|
||||||
}, [sortModel, currentUser]);
|
|
||||||
|
|
||||||
// Handle refetch flag
|
|
||||||
useEffect(() => {
|
|
||||||
if (refetch) {
|
|
||||||
loadData(0);
|
|
||||||
dispatch(setRefetchAction(false));
|
|
||||||
}
|
|
||||||
}, [refetch, dispatch, setRefetchAction]);
|
|
||||||
|
|
||||||
// Load columns when user is available
|
|
||||||
useEffect(() => {
|
|
||||||
if (!currentUser) return;
|
|
||||||
|
|
||||||
loadColumnsFunction(handleDeleteModalAction, entityName, currentUser).then(
|
|
||||||
(newCols) => setColumns(newCols),
|
|
||||||
);
|
|
||||||
}, [currentUser, entityName]);
|
|
||||||
|
|
||||||
// Modal handlers
|
|
||||||
const handleModalAction = () => {
|
|
||||||
setIsModalTrashActive(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteModalAction = (itemId: string) => {
|
|
||||||
setId(itemId);
|
|
||||||
setIsModalTrashActive(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteAction = async () => {
|
|
||||||
if (id) {
|
|
||||||
await dispatch(deleteAction(id));
|
|
||||||
await loadData(0);
|
|
||||||
setIsModalTrashActive(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Generate filter request
|
|
||||||
const generateFilterRequests = useMemo(() => {
|
|
||||||
let request = '&';
|
|
||||||
filterItems.forEach((item) => {
|
|
||||||
const isRangeFilter = filters.find(
|
|
||||||
(filter) =>
|
|
||||||
filter.title === item.fields.selectedField &&
|
|
||||||
(filter.number || filter.date),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isRangeFilter) {
|
|
||||||
const from = item.fields.filterValueFrom;
|
|
||||||
const to = item.fields.filterValueTo;
|
|
||||||
if (from) {
|
|
||||||
request += `${item.fields.selectedField}Range=${from}&`;
|
|
||||||
}
|
|
||||||
if (to) {
|
|
||||||
request += `${item.fields.selectedField}Range=${to}&`;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const value = item.fields.filterValue;
|
|
||||||
if (value) {
|
|
||||||
request += `${item.fields.selectedField}=${value}&`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return request;
|
|
||||||
}, [filterItems, filters]);
|
|
||||||
|
|
||||||
// Filter handlers
|
|
||||||
const deleteFilter = (value: string) => {
|
|
||||||
const newItems = filterItems.filter((item) => item.id !== value);
|
|
||||||
|
|
||||||
if (newItems.length) {
|
|
||||||
setFilterItems(newItems);
|
|
||||||
} else {
|
|
||||||
loadData(0, '');
|
|
||||||
setFilterItems(newItems);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = () => {
|
|
||||||
loadData(0, generateFilterRequests);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChange =
|
|
||||||
(itemId: string) =>
|
|
||||||
(e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
const name = e.target.name as keyof FilterFields;
|
|
||||||
|
|
||||||
setFilterItems(
|
|
||||||
filterItems.map((item) => {
|
|
||||||
if (item.id !== itemId) return item;
|
|
||||||
if (name === 'selectedField')
|
|
||||||
return {
|
|
||||||
id: itemId,
|
|
||||||
fields: {
|
|
||||||
selectedField: value,
|
|
||||||
filterValue: '',
|
|
||||||
filterValueFrom: '',
|
|
||||||
filterValueTo: '',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return { id: itemId, fields: { ...item.fields, [name]: value } };
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleReset = () => {
|
|
||||||
setFilterItems([]);
|
|
||||||
loadData(0, '');
|
|
||||||
};
|
|
||||||
|
|
||||||
const onPageChange = (page: number) => {
|
|
||||||
loadData(page);
|
|
||||||
setCurrentPage(page);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Table update handler
|
|
||||||
const handleTableSubmit = async (
|
|
||||||
rowId: string,
|
|
||||||
rowData: Record<string, unknown>,
|
|
||||||
) => {
|
|
||||||
if (!_.isEmpty(rowData) && updateAction) {
|
|
||||||
await dispatch(updateAction({ id: rowId, data: rowData as Partial<T> }))
|
|
||||||
.unwrap()
|
|
||||||
.then((res) => res)
|
|
||||||
.catch((err) => {
|
|
||||||
throw new Error(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Delete selected rows
|
|
||||||
const onDeleteRows = async (rows: GridRowSelectionModel) => {
|
|
||||||
if (deleteByIdsAction) {
|
|
||||||
await dispatch(deleteByIdsAction(rows as string[]));
|
|
||||||
await loadData(0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const controlClasses =
|
|
||||||
'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' +
|
|
||||||
` ${bgColor} ${focusRing} ${corners} ` +
|
|
||||||
'dark:bg-slate-800 border';
|
|
||||||
|
|
||||||
const dataGrid = (
|
|
||||||
<div className='relative overflow-x-auto'>
|
|
||||||
<DataGrid
|
|
||||||
autoHeight
|
|
||||||
rowHeight={64}
|
|
||||||
sx={dataGridStyles}
|
|
||||||
className={'datagrid--table'}
|
|
||||||
getRowClassName={() => 'datagrid--row'}
|
|
||||||
rows={data ?? []}
|
|
||||||
columns={columns}
|
|
||||||
initialState={{
|
|
||||||
pagination: {
|
|
||||||
paginationModel: {
|
|
||||||
pageSize: 10,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
disableRowSelectionOnClick
|
|
||||||
onProcessRowUpdateError={(params) => {
|
|
||||||
console.log('Error', params);
|
|
||||||
}}
|
|
||||||
processRowUpdate={async (newRow, oldRow) => {
|
|
||||||
const formattedData = dataFormatter.dataGridEditFormatter(newRow);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await handleTableSubmit(newRow.id, formattedData);
|
|
||||||
return newRow;
|
|
||||||
} catch {
|
|
||||||
return oldRow;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
sortingMode={'server'}
|
|
||||||
checkboxSelection
|
|
||||||
onRowSelectionModelChange={(ids) => {
|
|
||||||
setSelectedRows(ids);
|
|
||||||
}}
|
|
||||||
onSortModelChange={(params) => {
|
|
||||||
params.length
|
|
||||||
? setSortModel(params)
|
|
||||||
: setSortModel([{ field: '', sort: 'desc' }]);
|
|
||||||
}}
|
|
||||||
rowCount={count}
|
|
||||||
pageSizeOptions={[10]}
|
|
||||||
paginationMode={'server'}
|
|
||||||
loading={loading}
|
|
||||||
onPaginationModelChange={(params) => {
|
|
||||||
onPageChange(params.page);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{filterItems && Array.isArray(filterItems) && filterItems.length ? (
|
|
||||||
<CardBox>
|
|
||||||
<Formik
|
|
||||||
initialValues={{
|
|
||||||
checkboxes: ['lorem'],
|
|
||||||
switches: ['lorem'],
|
|
||||||
radio: 'lorem',
|
|
||||||
}}
|
|
||||||
onSubmit={() => null}
|
|
||||||
>
|
|
||||||
<Form>
|
|
||||||
<>
|
|
||||||
{filterItems.map((filterItem) => {
|
|
||||||
const selectedFilter = filters.find(
|
|
||||||
(filter) =>
|
|
||||||
filter.title === filterItem?.fields?.selectedField,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={filterItem.id} className='flex mb-4'>
|
|
||||||
<div className='flex flex-col w-full mr-3'>
|
|
||||||
<div className='text-gray-500 font-bold'>Filter</div>
|
|
||||||
<Field
|
|
||||||
className={controlClasses}
|
|
||||||
name='selectedField'
|
|
||||||
id='selectedField'
|
|
||||||
component='select'
|
|
||||||
value={filterItem?.fields?.selectedField || ''}
|
|
||||||
onChange={handleChange(filterItem.id)}
|
|
||||||
>
|
|
||||||
{filters.map((selectOption) => (
|
|
||||||
<option
|
|
||||||
key={selectOption.title}
|
|
||||||
value={selectOption.title}
|
|
||||||
>
|
|
||||||
{selectOption.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
{selectedFilter?.type === 'enum' ? (
|
|
||||||
<div className='flex flex-col w-full mr-3'>
|
|
||||||
<div className='text-gray-500 font-bold'>Value</div>
|
|
||||||
<Field
|
|
||||||
className={controlClasses}
|
|
||||||
name='filterValue'
|
|
||||||
id='filterValue'
|
|
||||||
component='select'
|
|
||||||
value={filterItem?.fields?.filterValue || ''}
|
|
||||||
onChange={handleChange(filterItem.id)}
|
|
||||||
>
|
|
||||||
<option value=''>Select Value</option>
|
|
||||||
{selectedFilter?.options?.map((option) => (
|
|
||||||
<option key={option} value={option}>
|
|
||||||
{option}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
) : selectedFilter?.number ? (
|
|
||||||
<div className='flex flex-row w-full mr-3'>
|
|
||||||
<div className='flex flex-col w-full mr-3'>
|
|
||||||
<div className='text-gray-500 font-bold'>From</div>
|
|
||||||
<Field
|
|
||||||
className={controlClasses}
|
|
||||||
name='filterValueFrom'
|
|
||||||
placeholder='From'
|
|
||||||
id='filterValueFrom'
|
|
||||||
value={filterItem?.fields?.filterValueFrom || ''}
|
|
||||||
onChange={handleChange(filterItem.id)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className='flex flex-col w-full'>
|
|
||||||
<div className='text-gray-500 font-bold'>To</div>
|
|
||||||
<Field
|
|
||||||
className={controlClasses}
|
|
||||||
name='filterValueTo'
|
|
||||||
placeholder='to'
|
|
||||||
id='filterValueTo'
|
|
||||||
value={filterItem?.fields?.filterValueTo || ''}
|
|
||||||
onChange={handleChange(filterItem.id)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : selectedFilter?.date ? (
|
|
||||||
<div className='flex flex-row w-full mr-3'>
|
|
||||||
<div className='flex flex-col w-full mr-3'>
|
|
||||||
<div className='text-gray-500 font-bold'>From</div>
|
|
||||||
<Field
|
|
||||||
className={controlClasses}
|
|
||||||
name='filterValueFrom'
|
|
||||||
placeholder='From'
|
|
||||||
id='filterValueFrom'
|
|
||||||
type='datetime-local'
|
|
||||||
value={filterItem?.fields?.filterValueFrom || ''}
|
|
||||||
onChange={handleChange(filterItem.id)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className='flex flex-col w-full'>
|
|
||||||
<div className='text-gray-500 font-bold'>To</div>
|
|
||||||
<Field
|
|
||||||
className={controlClasses}
|
|
||||||
name='filterValueTo'
|
|
||||||
placeholder='to'
|
|
||||||
id='filterValueTo'
|
|
||||||
type='datetime-local'
|
|
||||||
value={filterItem?.fields?.filterValueTo || ''}
|
|
||||||
onChange={handleChange(filterItem.id)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className='flex flex-col w-full mr-3'>
|
|
||||||
<div className='text-gray-500 font-bold'>
|
|
||||||
Contains
|
|
||||||
</div>
|
|
||||||
<Field
|
|
||||||
className={controlClasses}
|
|
||||||
name='filterValue'
|
|
||||||
placeholder='Contained'
|
|
||||||
id='filterValue'
|
|
||||||
value={filterItem?.fields?.filterValue || ''}
|
|
||||||
onChange={handleChange(filterItem.id)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className='flex flex-col'>
|
|
||||||
<div className='text-gray-500 font-bold'>Action</div>
|
|
||||||
<BaseButton
|
|
||||||
className='my-2'
|
|
||||||
type='reset'
|
|
||||||
color='danger'
|
|
||||||
label='Delete'
|
|
||||||
onClick={() => {
|
|
||||||
deleteFilter(filterItem.id);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<div className='flex'>
|
|
||||||
<BaseButton
|
|
||||||
className='my-2 mr-3'
|
|
||||||
color='success'
|
|
||||||
label='Apply'
|
|
||||||
onClick={handleSubmit}
|
|
||||||
/>
|
|
||||||
<BaseButton
|
|
||||||
className='my-2'
|
|
||||||
color='info'
|
|
||||||
label='Cancel'
|
|
||||||
onClick={handleReset}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
</Form>
|
|
||||||
</Formik>
|
|
||||||
</CardBox>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<CardBoxModal
|
|
||||||
title='Please confirm'
|
|
||||||
buttonColor='info'
|
|
||||||
buttonLabel={loading ? 'Deleting...' : 'Confirm'}
|
|
||||||
isActive={isModalTrashActive}
|
|
||||||
onConfirm={handleDeleteAction}
|
|
||||||
onCancel={handleModalAction}
|
|
||||||
>
|
|
||||||
<p>Are you sure you want to delete this item?</p>
|
|
||||||
</CardBoxModal>
|
|
||||||
|
|
||||||
{dataGrid}
|
|
||||||
|
|
||||||
{selectedRows.length > 0 &&
|
|
||||||
typeof document !== 'undefined' &&
|
|
||||||
document.getElementById('delete-rows-button') &&
|
|
||||||
createPortal(
|
|
||||||
<BaseButton
|
|
||||||
className='me-4'
|
|
||||||
color='danger'
|
|
||||||
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`}
|
|
||||||
onClick={() => onDeleteRows(selectedRows)}
|
|
||||||
/>,
|
|
||||||
document.getElementById('delete-rows-button')!,
|
|
||||||
)}
|
|
||||||
<ToastContainer />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default GenericTable;
|
|
||||||
@ -5,7 +5,7 @@ import axios from 'axios';
|
|||||||
import {
|
import {
|
||||||
GridActionsCellItem,
|
GridActionsCellItem,
|
||||||
GridRowParams,
|
GridRowParams,
|
||||||
GridValueGetterParams,
|
|
||||||
} from '@mui/x-data-grid';
|
} from '@mui/x-data-grid';
|
||||||
import ImageField from '../ImageField';
|
import ImageField from '../ImageField';
|
||||||
import { saveFile } from '../../helpers/fileSaver';
|
import { saveFile } from '../../helpers/fileSaver';
|
||||||
@ -54,8 +54,7 @@ export const loadColumns = async (
|
|||||||
getOptionValue: (value: any) => value?.id,
|
getOptionValue: (value: any) => value?.id,
|
||||||
getOptionLabel: (value: any) => value?.label,
|
getOptionLabel: (value: any) => value?.label,
|
||||||
valueOptions: await callOptionsApi('tour_pages'),
|
valueOptions: await callOptionsApi('tour_pages'),
|
||||||
valueGetter: (params: GridValueGetterParams) =>
|
valueGetter: (value) => value?.id ?? value,
|
||||||
params?.value?.id ?? params?.value,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import axios from 'axios';
|
|||||||
import {
|
import {
|
||||||
GridActionsCellItem,
|
GridActionsCellItem,
|
||||||
GridRowParams,
|
GridRowParams,
|
||||||
GridValueGetterParams,
|
|
||||||
} from '@mui/x-data-grid';
|
} from '@mui/x-data-grid';
|
||||||
import ImageField from '../ImageField';
|
import ImageField from '../ImageField';
|
||||||
import { saveFile } from '../../helpers/fileSaver';
|
import { saveFile } from '../../helpers/fileSaver';
|
||||||
@ -54,8 +54,7 @@ export const loadColumns = async (
|
|||||||
getOptionValue: (value: any) => value?.id,
|
getOptionValue: (value: any) => value?.id,
|
||||||
getOptionLabel: (value: any) => value?.label,
|
getOptionLabel: (value: any) => value?.label,
|
||||||
valueOptions: await callOptionsApi('tour_pages'),
|
valueOptions: await callOptionsApi('tour_pages'),
|
||||||
valueGetter: (params: GridValueGetterParams) =>
|
valueGetter: (value) => value?.id ?? value,
|
||||||
params?.value?.id ?? params?.value,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -74,8 +73,7 @@ export const loadColumns = async (
|
|||||||
getOptionValue: (value: any) => value?.id,
|
getOptionValue: (value: any) => value?.id,
|
||||||
getOptionLabel: (value: any) => value?.label,
|
getOptionLabel: (value: any) => value?.label,
|
||||||
valueOptions: await callOptionsApi('tour_pages'),
|
valueOptions: await callOptionsApi('tour_pages'),
|
||||||
valueGetter: (params: GridValueGetterParams) =>
|
valueGetter: (value) => value?.id ?? value,
|
||||||
params?.value?.id ?? params?.value,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -118,8 +116,7 @@ export const loadColumns = async (
|
|||||||
getOptionValue: (value: any) => value?.id,
|
getOptionValue: (value: any) => value?.id,
|
||||||
getOptionLabel: (value: any) => value?.label,
|
getOptionLabel: (value: any) => value?.label,
|
||||||
valueOptions: await callOptionsApi('transitions'),
|
valueOptions: await callOptionsApi('transitions'),
|
||||||
valueGetter: (params: GridValueGetterParams) =>
|
valueGetter: (value) => value?.id ?? value,
|
||||||
params?.value?.id ?? params?.value,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import axios from 'axios';
|
|||||||
import {
|
import {
|
||||||
GridActionsCellItem,
|
GridActionsCellItem,
|
||||||
GridRowParams,
|
GridRowParams,
|
||||||
GridValueGetterParams,
|
|
||||||
} from '@mui/x-data-grid';
|
} from '@mui/x-data-grid';
|
||||||
import ImageField from '../ImageField';
|
import ImageField from '../ImageField';
|
||||||
import { saveFile } from '../../helpers/fileSaver';
|
import { saveFile } from '../../helpers/fileSaver';
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import axios from 'axios';
|
|||||||
import {
|
import {
|
||||||
GridActionsCellItem,
|
GridActionsCellItem,
|
||||||
GridRowParams,
|
GridRowParams,
|
||||||
GridValueGetterParams,
|
|
||||||
} from '@mui/x-data-grid';
|
} from '@mui/x-data-grid';
|
||||||
import ImageField from '../ImageField';
|
import ImageField from '../ImageField';
|
||||||
import { saveFile } from '../../helpers/fileSaver';
|
import { saveFile } from '../../helpers/fileSaver';
|
||||||
@ -57,8 +57,7 @@ export const loadColumns = async (
|
|||||||
getOptionValue: (value: any) => value?.id,
|
getOptionValue: (value: any) => value?.id,
|
||||||
getOptionLabel: (value: any) => value?.label,
|
getOptionLabel: (value: any) => value?.label,
|
||||||
valueOptions: await callOptionsApi('projects'),
|
valueOptions: await callOptionsApi('projects'),
|
||||||
valueGetter: (params: GridValueGetterParams) =>
|
valueGetter: (value) => value?.id ?? value,
|
||||||
params?.value?.id ?? params?.value,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -77,8 +76,7 @@ export const loadColumns = async (
|
|||||||
getOptionValue: (value: any) => value?.id,
|
getOptionValue: (value: any) => value?.id,
|
||||||
getOptionLabel: (value: any) => value?.label,
|
getOptionLabel: (value: any) => value?.label,
|
||||||
valueOptions: await callOptionsApi('users'),
|
valueOptions: await callOptionsApi('users'),
|
||||||
valueGetter: (params: GridValueGetterParams) =>
|
valueGetter: (value) => value?.id ?? value,
|
||||||
params?.value?.id ?? params?.value,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -155,8 +153,7 @@ export const loadColumns = async (
|
|||||||
editable: hasUpdatePermission,
|
editable: hasUpdatePermission,
|
||||||
|
|
||||||
type: 'dateTime',
|
type: 'dateTime',
|
||||||
valueGetter: (params: GridValueGetterParams) =>
|
valueGetter: (_value, row) => new Date(row.expires_at),
|
||||||
new Date(params.row.expires_at),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import axios from 'axios';
|
|||||||
import {
|
import {
|
||||||
GridActionsCellItem,
|
GridActionsCellItem,
|
||||||
GridRowParams,
|
GridRowParams,
|
||||||
GridValueGetterParams,
|
|
||||||
} from '@mui/x-data-grid';
|
} from '@mui/x-data-grid';
|
||||||
import ImageField from '../ImageField';
|
import ImageField from '../ImageField';
|
||||||
import { saveFile } from '../../helpers/fileSaver';
|
import { saveFile } from '../../helpers/fileSaver';
|
||||||
@ -57,8 +57,7 @@ export const loadColumns = async (
|
|||||||
getOptionValue: (value: any) => value?.id,
|
getOptionValue: (value: any) => value?.id,
|
||||||
getOptionLabel: (value: any) => value?.label,
|
getOptionLabel: (value: any) => value?.label,
|
||||||
valueOptions: await callOptionsApi('projects'),
|
valueOptions: await callOptionsApi('projects'),
|
||||||
valueGetter: (params: GridValueGetterParams) =>
|
valueGetter: (value) => value?.id ?? value,
|
||||||
params?.value?.id ?? params?.value,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import axios from 'axios';
|
|||||||
import {
|
import {
|
||||||
GridActionsCellItem,
|
GridActionsCellItem,
|
||||||
GridRowParams,
|
GridRowParams,
|
||||||
GridValueGetterParams,
|
|
||||||
} from '@mui/x-data-grid';
|
} from '@mui/x-data-grid';
|
||||||
import ImageField from '../ImageField';
|
import ImageField from '../ImageField';
|
||||||
import { saveFile } from '../../helpers/fileSaver';
|
import { saveFile } from '../../helpers/fileSaver';
|
||||||
@ -54,8 +54,7 @@ export const loadColumns = async (
|
|||||||
getOptionValue: (value: any) => value?.id,
|
getOptionValue: (value: any) => value?.id,
|
||||||
getOptionLabel: (value: any) => value?.label,
|
getOptionLabel: (value: any) => value?.label,
|
||||||
valueOptions: await callOptionsApi('projects'),
|
valueOptions: await callOptionsApi('projects'),
|
||||||
valueGetter: (params: GridValueGetterParams) =>
|
valueGetter: (value) => value?.id ?? value,
|
||||||
params?.value?.id ?? params?.value,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -74,8 +73,7 @@ export const loadColumns = async (
|
|||||||
getOptionValue: (value: any) => value?.id,
|
getOptionValue: (value: any) => value?.id,
|
||||||
getOptionLabel: (value: any) => value?.label,
|
getOptionLabel: (value: any) => value?.label,
|
||||||
valueOptions: await callOptionsApi('users'),
|
valueOptions: await callOptionsApi('users'),
|
||||||
valueGetter: (params: GridValueGetterParams) =>
|
valueGetter: (value) => value?.id ?? value,
|
||||||
params?.value?.id ?? params?.value,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -116,8 +114,7 @@ export const loadColumns = async (
|
|||||||
editable: hasUpdatePermission,
|
editable: hasUpdatePermission,
|
||||||
|
|
||||||
type: 'dateTime',
|
type: 'dateTime',
|
||||||
valueGetter: (params: GridValueGetterParams) =>
|
valueGetter: (_value, row) => new Date(row.invited_at),
|
||||||
new Date(params.row.invited_at),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -132,8 +129,7 @@ export const loadColumns = async (
|
|||||||
editable: hasUpdatePermission,
|
editable: hasUpdatePermission,
|
||||||
|
|
||||||
type: 'dateTime',
|
type: 'dateTime',
|
||||||
valueGetter: (params: GridValueGetterParams) =>
|
valueGetter: (_value, row) => new Date(row.accepted_at),
|
||||||
new Date(params.row.accepted_at),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import axios from 'axios';
|
|||||||
import {
|
import {
|
||||||
GridActionsCellItem,
|
GridActionsCellItem,
|
||||||
GridRowParams,
|
GridRowParams,
|
||||||
GridValueGetterParams,
|
|
||||||
} from '@mui/x-data-grid';
|
} from '@mui/x-data-grid';
|
||||||
import ImageField from '../ImageField';
|
import ImageField from '../ImageField';
|
||||||
import { saveFile } from '../../helpers/fileSaver';
|
import { saveFile } from '../../helpers/fileSaver';
|
||||||
@ -196,8 +196,7 @@ export const loadColumns = async (
|
|||||||
editable: hasUpdatePermission,
|
editable: hasUpdatePermission,
|
||||||
|
|
||||||
type: 'dateTime',
|
type: 'dateTime',
|
||||||
valueGetter: (params: GridValueGetterParams) =>
|
valueGetter: (_value, row) => new Date(row.deleted_at_time),
|
||||||
new Date(params.row.deleted_at_time),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import axios from 'axios';
|
|||||||
import {
|
import {
|
||||||
GridActionsCellItem,
|
GridActionsCellItem,
|
||||||
GridRowParams,
|
GridRowParams,
|
||||||
GridValueGetterParams,
|
|
||||||
} from '@mui/x-data-grid';
|
} from '@mui/x-data-grid';
|
||||||
import ImageField from '../ImageField';
|
import ImageField from '../ImageField';
|
||||||
import { saveFile } from '../../helpers/fileSaver';
|
import { saveFile } from '../../helpers/fileSaver';
|
||||||
@ -54,8 +54,7 @@ export const loadColumns = async (
|
|||||||
getOptionValue: (value: any) => value?.id,
|
getOptionValue: (value: any) => value?.id,
|
||||||
getOptionLabel: (value: any) => value?.label,
|
getOptionLabel: (value: any) => value?.label,
|
||||||
valueOptions: await callOptionsApi('projects'),
|
valueOptions: await callOptionsApi('projects'),
|
||||||
valueGetter: (params: GridValueGetterParams) =>
|
valueGetter: (value) => value?.id ?? value,
|
||||||
params?.value?.id ?? params?.value,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -74,8 +73,7 @@ export const loadColumns = async (
|
|||||||
getOptionValue: (value: any) => value?.id,
|
getOptionValue: (value: any) => value?.id,
|
||||||
getOptionLabel: (value: any) => value?.label,
|
getOptionLabel: (value: any) => value?.label,
|
||||||
valueOptions: await callOptionsApi('users'),
|
valueOptions: await callOptionsApi('users'),
|
||||||
valueGetter: (params: GridValueGetterParams) =>
|
valueGetter: (value) => value?.id ?? value,
|
||||||
params?.value?.id ?? params?.value,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -114,8 +112,7 @@ export const loadColumns = async (
|
|||||||
editable: hasUpdatePermission,
|
editable: hasUpdatePermission,
|
||||||
|
|
||||||
type: 'dateTime',
|
type: 'dateTime',
|
||||||
valueGetter: (params: GridValueGetterParams) =>
|
valueGetter: (_value, row) => new Date(row.started_at),
|
||||||
new Date(params.row.started_at),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -130,8 +127,7 @@ export const loadColumns = async (
|
|||||||
editable: hasUpdatePermission,
|
editable: hasUpdatePermission,
|
||||||
|
|
||||||
type: 'dateTime',
|
type: 'dateTime',
|
||||||
valueGetter: (params: GridValueGetterParams) =>
|
valueGetter: (_value, row) => new Date(row.finished_at),
|
||||||
new Date(params.row.finished_at),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import axios from 'axios';
|
|||||||
import {
|
import {
|
||||||
GridActionsCellItem,
|
GridActionsCellItem,
|
||||||
GridRowParams,
|
GridRowParams,
|
||||||
GridValueGetterParams,
|
|
||||||
} from '@mui/x-data-grid';
|
} from '@mui/x-data-grid';
|
||||||
import ImageField from '../ImageField';
|
import ImageField from '../ImageField';
|
||||||
import { saveFile } from '../../helpers/fileSaver';
|
import { saveFile } from '../../helpers/fileSaver';
|
||||||
@ -54,8 +54,7 @@ export const loadColumns = async (
|
|||||||
getOptionValue: (value: any) => value?.id,
|
getOptionValue: (value: any) => value?.id,
|
||||||
getOptionLabel: (value: any) => value?.label,
|
getOptionLabel: (value: any) => value?.label,
|
||||||
valueOptions: await callOptionsApi('projects'),
|
valueOptions: await callOptionsApi('projects'),
|
||||||
valueGetter: (params: GridValueGetterParams) =>
|
valueGetter: (value) => value?.id ?? value,
|
||||||
params?.value?.id ?? params?.value,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -118,8 +117,7 @@ export const loadColumns = async (
|
|||||||
editable: hasUpdatePermission,
|
editable: hasUpdatePermission,
|
||||||
|
|
||||||
type: 'dateTime',
|
type: 'dateTime',
|
||||||
valueGetter: (params: GridValueGetterParams) =>
|
valueGetter: (_value, row) => new Date(row.generated_at),
|
||||||
new Date(params.row.generated_at),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import axios from 'axios';
|
|||||||
import {
|
import {
|
||||||
GridActionsCellItem,
|
GridActionsCellItem,
|
||||||
GridRowParams,
|
GridRowParams,
|
||||||
GridValueGetterParams,
|
|
||||||
} from '@mui/x-data-grid';
|
} from '@mui/x-data-grid';
|
||||||
import ImageField from '../ImageField';
|
import ImageField from '../ImageField';
|
||||||
import { saveFile } from '../../helpers/fileSaver';
|
import { saveFile } from '../../helpers/fileSaver';
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import axios from 'axios';
|
|||||||
import {
|
import {
|
||||||
GridActionsCellItem,
|
GridActionsCellItem,
|
||||||
GridRowParams,
|
GridRowParams,
|
||||||
GridValueGetterParams,
|
|
||||||
} from '@mui/x-data-grid';
|
} from '@mui/x-data-grid';
|
||||||
import ImageField from '../ImageField';
|
import ImageField from '../ImageField';
|
||||||
import { saveFile } from '../../helpers/fileSaver';
|
import { saveFile } from '../../helpers/fileSaver';
|
||||||
@ -54,8 +54,7 @@ export const loadColumns = async (
|
|||||||
getOptionValue: (value: any) => value?.id,
|
getOptionValue: (value: any) => value?.id,
|
||||||
getOptionLabel: (value: any) => value?.label,
|
getOptionLabel: (value: any) => value?.label,
|
||||||
valueOptions: await callOptionsApi('projects'),
|
valueOptions: await callOptionsApi('projects'),
|
||||||
valueGetter: (params: GridValueGetterParams) =>
|
valueGetter: (value) => value?.id ?? value,
|
||||||
params?.value?.id ?? params?.value,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import axios from 'axios';
|
|||||||
import {
|
import {
|
||||||
GridActionsCellItem,
|
GridActionsCellItem,
|
||||||
GridRowParams,
|
GridRowParams,
|
||||||
GridValueGetterParams,
|
|
||||||
} from '@mui/x-data-grid';
|
} from '@mui/x-data-grid';
|
||||||
import ImageField from '../ImageField';
|
import ImageField from '../ImageField';
|
||||||
import { saveFile } from '../../helpers/fileSaver';
|
import { saveFile } from '../../helpers/fileSaver';
|
||||||
@ -54,8 +54,7 @@ export const loadColumns = async (
|
|||||||
getOptionValue: (value: any) => value?.id,
|
getOptionValue: (value: any) => value?.id,
|
||||||
getOptionLabel: (value: any) => value?.label,
|
getOptionLabel: (value: any) => value?.label,
|
||||||
valueOptions: await callOptionsApi('projects'),
|
valueOptions: await callOptionsApi('projects'),
|
||||||
valueGetter: (params: GridValueGetterParams) =>
|
valueGetter: (value) => value?.id ?? value,
|
||||||
params?.value?.id ?? params?.value,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@ -75,19 +75,28 @@ export default class FileUploader {
|
|||||||
typeof options.onProgress === 'function' ? options.onProgress : null;
|
typeof options.onProgress === 'function' ? options.onProgress : null;
|
||||||
const onStatus =
|
const onStatus =
|
||||||
typeof options.onStatus === 'function' ? options.onStatus : null;
|
typeof options.onStatus === 'function' ? options.onStatus : null;
|
||||||
|
const signal = options.signal || null;
|
||||||
const extension = extractExtensionFrom(file.name);
|
const extension = extractExtensionFrom(file.name);
|
||||||
const id = uuidv4();
|
const id = uuidv4();
|
||||||
const filename = extension ? `${id}.${extension}` : id;
|
const filename = extension ? `${id}.${extension}` : id;
|
||||||
const privateUrl = `${path}/${filename}`;
|
const privateUrl = `${path}/${filename}`;
|
||||||
const totalChunks = Math.max(1, Math.ceil(file.size / chunkSize));
|
const totalChunks = Math.max(1, Math.ceil(file.size / chunkSize));
|
||||||
|
|
||||||
const initResponse = await Axios.post('/file/upload-sessions/init', {
|
if (signal?.aborted) {
|
||||||
folder: path,
|
throw new Error('Upload aborted');
|
||||||
filename,
|
}
|
||||||
size: file.size,
|
|
||||||
contentType: file.type || '',
|
const initResponse = await Axios.post(
|
||||||
totalChunks,
|
'/file/upload-sessions/init',
|
||||||
});
|
{
|
||||||
|
folder: path,
|
||||||
|
filename,
|
||||||
|
size: file.size,
|
||||||
|
contentType: file.type || '',
|
||||||
|
totalChunks,
|
||||||
|
},
|
||||||
|
{ signal },
|
||||||
|
);
|
||||||
|
|
||||||
const sessionId = initResponse?.data?.sessionId;
|
const sessionId = initResponse?.data?.sessionId;
|
||||||
|
|
||||||
@ -96,6 +105,10 @@ export default class FileUploader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex += 1) {
|
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex += 1) {
|
||||||
|
if (signal?.aborted) {
|
||||||
|
throw new Error('Upload aborted');
|
||||||
|
}
|
||||||
|
|
||||||
const start = chunkIndex * chunkSize;
|
const start = chunkIndex * chunkSize;
|
||||||
const end = Math.min(file.size, start + chunkSize);
|
const end = Math.min(file.size, start + chunkSize);
|
||||||
const chunk = file.slice(start, end);
|
const chunk = file.slice(start, end);
|
||||||
@ -118,10 +131,14 @@ export default class FileUploader {
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/octet-stream',
|
'Content-Type': 'application/octet-stream',
|
||||||
},
|
},
|
||||||
|
signal,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (signal?.aborted) {
|
||||||
|
throw new Error('Upload aborted');
|
||||||
|
}
|
||||||
retry += 1;
|
retry += 1;
|
||||||
if (retry > maxRetries) {
|
if (retry > maxRetries) {
|
||||||
throw error;
|
throw error;
|
||||||
@ -139,12 +156,18 @@ export default class FileUploader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (signal?.aborted) {
|
||||||
|
throw new Error('Upload aborted');
|
||||||
|
}
|
||||||
|
|
||||||
if (onStatus) {
|
if (onStatus) {
|
||||||
onStatus('finalizing', null);
|
onStatus('finalizing', null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalizeResponse = await Axios.post(
|
const finalizeResponse = await Axios.post(
|
||||||
`/file/upload-sessions/${sessionId}/finalize`,
|
`/file/upload-sessions/${sessionId}/finalize`,
|
||||||
|
null,
|
||||||
|
{ signal },
|
||||||
);
|
);
|
||||||
const responsePublicUrl = finalizeResponse?.data?.url;
|
const responsePublicUrl = finalizeResponse?.data?.url;
|
||||||
const publicUrl = responsePublicUrl
|
const publicUrl = responsePublicUrl
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import axios from 'axios';
|
|||||||
import {
|
import {
|
||||||
GridActionsCellItem,
|
GridActionsCellItem,
|
||||||
GridRowParams,
|
GridRowParams,
|
||||||
GridValueGetterParams,
|
|
||||||
} from '@mui/x-data-grid';
|
} from '@mui/x-data-grid';
|
||||||
import ImageField from '../ImageField';
|
import ImageField from '../ImageField';
|
||||||
import { saveFile } from '../../helpers/fileSaver';
|
import { saveFile } from '../../helpers/fileSaver';
|
||||||
@ -111,7 +111,7 @@ export const loadColumns = async (
|
|||||||
|
|
||||||
editable: false,
|
editable: false,
|
||||||
sortable: false,
|
sortable: false,
|
||||||
renderCell: (params: GridValueGetterParams) => (
|
renderCell: (value) => (
|
||||||
<ImageField
|
<ImageField
|
||||||
name={'Avatar'}
|
name={'Avatar'}
|
||||||
image={params?.row?.avatar}
|
image={params?.row?.avatar}
|
||||||
@ -136,8 +136,7 @@ export const loadColumns = async (
|
|||||||
getOptionValue: (value: any) => value?.id,
|
getOptionValue: (value: any) => value?.id,
|
||||||
getOptionLabel: (value: any) => value?.label,
|
getOptionLabel: (value: any) => value?.label,
|
||||||
valueOptions: await callOptionsApi('roles'),
|
valueOptions: await callOptionsApi('roles'),
|
||||||
valueGetter: (params: GridValueGetterParams) =>
|
valueGetter: (value) => value?.id ?? value,
|
||||||
params?.value?.id ?? params?.value,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,64 +1,17 @@
|
|||||||
import { mdiChartTimelineVariant } from '@mdi/js';
|
import { mdiChartTimelineVariant } from '@mdi/js';
|
||||||
import axios from 'axios';
|
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import { useRouter } from 'next/router';
|
import React, { ReactElement, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import React, { ReactElement, useEffect, useMemo, useState } from 'react';
|
|
||||||
import { toast, ToastContainer } from 'react-toastify';
|
import { toast, ToastContainer } from 'react-toastify';
|
||||||
import BaseButton from '../../components/BaseButton';
|
|
||||||
import CardBox from '../../components/CardBox';
|
|
||||||
import FileUploader from '../../components/Uploaders/UploadService';
|
|
||||||
import LayoutAuthenticated from '../../layouts/Authenticated';
|
import LayoutAuthenticated from '../../layouts/Authenticated';
|
||||||
import SectionMain from '../../components/SectionMain';
|
import SectionMain from '../../components/SectionMain';
|
||||||
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
|
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
|
||||||
import { getPageTitle } from '../../config';
|
import { getPageTitle } from '../../config';
|
||||||
import { hasPermission } from '../../helpers/userPermissions';
|
import { hasPermission } from '../../helpers/userPermissions';
|
||||||
import { useAppSelector } from '../../stores/hooks';
|
import { useAppDispatch, useAppSelector } from '../../stores/hooks';
|
||||||
|
import { fetch as fetchAssets, deleteItem as deleteAsset } from '../../stores/assets/assetsSlice';
|
||||||
type Project = {
|
import AssetSectionCard, { Asset, AssetSection } from '../../components/Assets/AssetSectionCard';
|
||||||
id: string;
|
import { useProjectSelector } from '../../components/Assets/ProjectSelector';
|
||||||
name: string;
|
import { useAssetUploader } from '../../components/Assets/useAssetUploader';
|
||||||
};
|
|
||||||
|
|
||||||
type Asset = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
asset_type: 'image' | 'video' | 'audio' | 'file';
|
|
||||||
type?:
|
|
||||||
| 'icon'
|
|
||||||
| 'background_image'
|
|
||||||
| 'audio'
|
|
||||||
| 'video'
|
|
||||||
| 'transition'
|
|
||||||
| 'logo'
|
|
||||||
| 'favicon'
|
|
||||||
| 'document'
|
|
||||||
| 'general';
|
|
||||||
cdn_url?: string | null;
|
|
||||||
mime_type?: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
type AssetSection = {
|
|
||||||
key:
|
|
||||||
| 'images'
|
|
||||||
| 'backgroundImages'
|
|
||||||
| 'audio'
|
|
||||||
| 'video'
|
|
||||||
| 'transitions'
|
|
||||||
| 'logo';
|
|
||||||
label: string;
|
|
||||||
accept: string;
|
|
||||||
assetFormat: 'image' | 'video' | 'audio';
|
|
||||||
assetCategory: NonNullable<Asset['type']>;
|
|
||||||
legacyTag: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type UploadQueueItem = {
|
|
||||||
id: string;
|
|
||||||
fileName: string;
|
|
||||||
progress: number;
|
|
||||||
status: 'queued' | 'uploading' | 'saving' | 'success' | 'error';
|
|
||||||
error?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ASSET_SECTIONS: AssetSection[] = [
|
const ASSET_SECTIONS: AssetSection[] = [
|
||||||
{
|
{
|
||||||
@ -112,29 +65,39 @@ const ASSET_SECTIONS: AssetSection[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const AssetsTablesPage = () => {
|
const AssetsTablesPage = () => {
|
||||||
const router = useRouter();
|
const dispatch = useAppDispatch();
|
||||||
const routeProjectId = useMemo(() => {
|
|
||||||
const value = router.query.projectId;
|
|
||||||
if (Array.isArray(value)) return value[0] || '';
|
|
||||||
return String(value || '');
|
|
||||||
}, [router.query.projectId]);
|
|
||||||
|
|
||||||
const { currentUser } = useAppSelector((state) => state.auth);
|
const { currentUser } = useAppSelector((state) => state.auth);
|
||||||
|
const assets = useAppSelector((state) => state.assets.assets) as Asset[];
|
||||||
|
const isLoadingAssets = useAppSelector((state) => state.assets.loading);
|
||||||
|
|
||||||
const [projects, setProjects] = useState<Project[]>([]);
|
|
||||||
const [selectedProjectId, setSelectedProjectId] = useState('');
|
|
||||||
const [isLoadingProjects, setIsLoadingProjects] = useState(false);
|
|
||||||
const [assets, setAssets] = useState<Asset[]>([]);
|
|
||||||
const [isLoadingAssets, setIsLoadingAssets] = useState(false);
|
|
||||||
const [uploadingSections, setUploadingSections] = useState<string[]>([]);
|
|
||||||
const [uploadQueues, setUploadQueues] = useState<
|
|
||||||
Record<string, UploadQueueItem[]>
|
|
||||||
>({});
|
|
||||||
const [expandedUploadedLists, setExpandedUploadedLists] = useState<
|
|
||||||
Record<string, boolean>
|
|
||||||
>({});
|
|
||||||
const [deletingAssetId, setDeletingAssetId] = useState<string>('');
|
const [deletingAssetId, setDeletingAssetId] = useState<string>('');
|
||||||
|
|
||||||
|
const {
|
||||||
|
selectedProjectId,
|
||||||
|
isLoadingProjects,
|
||||||
|
selectedProjectName,
|
||||||
|
} = useProjectSelector({ currentUser });
|
||||||
|
|
||||||
|
const loadAssets = useCallback((projectId: string) => {
|
||||||
|
if (!projectId) return;
|
||||||
|
dispatch(fetchAssets({
|
||||||
|
query: `?limit=500&page=0&sort=desc&field=createdAt&project=${projectId}`,
|
||||||
|
}));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
uploadingSections,
|
||||||
|
uploadQueues,
|
||||||
|
runBatchUpload,
|
||||||
|
} = useAssetUploader({
|
||||||
|
selectedProjectId,
|
||||||
|
onUploadComplete: () => loadAssets(selectedProjectId),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadAssets(selectedProjectId);
|
||||||
|
}, [selectedProjectId, loadAssets]);
|
||||||
|
|
||||||
const hasCreatePermission = Boolean(
|
const hasCreatePermission = Boolean(
|
||||||
currentUser && hasPermission(currentUser, 'CREATE_ASSETS'),
|
currentUser && hasPermission(currentUser, 'CREATE_ASSETS'),
|
||||||
);
|
);
|
||||||
@ -142,286 +105,16 @@ const AssetsTablesPage = () => {
|
|||||||
currentUser && hasPermission(currentUser, 'DELETE_ASSETS'),
|
currentUser && hasPermission(currentUser, 'DELETE_ASSETS'),
|
||||||
);
|
);
|
||||||
|
|
||||||
const loadProjects = async () => {
|
const handleDeleteAsset = useCallback(async (assetId: string) => {
|
||||||
setIsLoadingProjects(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
let rows: Project[] = [];
|
|
||||||
const response = await axios.get(
|
|
||||||
'/projects?limit=100&page=0&sort=desc&field=updatedAt',
|
|
||||||
);
|
|
||||||
rows = Array.isArray(response?.data?.rows) ? response.data.rows : [];
|
|
||||||
|
|
||||||
if (rows.length === 0) {
|
|
||||||
const autocompleteResponse = await axios.get(
|
|
||||||
'/projects/autocomplete?limit=100',
|
|
||||||
);
|
|
||||||
const autocompleteItems = Array.isArray(autocompleteResponse?.data)
|
|
||||||
? autocompleteResponse.data
|
|
||||||
: [];
|
|
||||||
rows = autocompleteItems.map((item: { id: string; label: string }) => ({
|
|
||||||
id: item.id,
|
|
||||||
name: item.label,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect to projects page if no projects exist
|
|
||||||
if (rows.length === 0) {
|
|
||||||
toast('Please create a project first', {
|
|
||||||
type: 'info',
|
|
||||||
position: 'bottom-center',
|
|
||||||
});
|
|
||||||
router.replace('/projects/projects-new');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setProjects(rows);
|
|
||||||
|
|
||||||
if (
|
|
||||||
routeProjectId &&
|
|
||||||
rows.some((project) => project.id === routeProjectId)
|
|
||||||
) {
|
|
||||||
setSelectedProjectId(routeProjectId);
|
|
||||||
} else {
|
|
||||||
setSelectedProjectId((prev) => {
|
|
||||||
if (rows.some((project) => project.id === prev)) return prev;
|
|
||||||
return rows[0]?.id || '';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Failed to load projects:', error?.message || error);
|
|
||||||
setProjects([]);
|
|
||||||
setSelectedProjectId('');
|
|
||||||
toast('Failed to load projects', {
|
|
||||||
type: 'error',
|
|
||||||
position: 'bottom-center',
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsLoadingProjects(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadAssets = async (projectId: string) => {
|
|
||||||
if (!projectId) {
|
|
||||||
setAssets([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoadingAssets(true);
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
`/assets?limit=500&page=0&sort=desc&field=createdAt&project=${projectId}`,
|
|
||||||
);
|
|
||||||
const rows = Array.isArray(response?.data?.rows)
|
|
||||||
? response.data.rows
|
|
||||||
: [];
|
|
||||||
setAssets(rows);
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Failed to load assets:', error?.message || error);
|
|
||||||
toast('Failed to load assets', {
|
|
||||||
type: 'error',
|
|
||||||
position: 'bottom-center',
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsLoadingAssets(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Wait for auth to be established before loading data
|
|
||||||
if (!currentUser) return;
|
|
||||||
loadProjects();
|
|
||||||
}, [routeProjectId, currentUser]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadAssets(selectedProjectId);
|
|
||||||
}, [selectedProjectId]);
|
|
||||||
|
|
||||||
const addSectionUpload = (sectionKey: string, items: UploadQueueItem[]) => {
|
|
||||||
setUploadQueues((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[sectionKey]: [...(prev[sectionKey] || []), ...items],
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateSectionUpload = (
|
|
||||||
sectionKey: string,
|
|
||||||
itemId: string,
|
|
||||||
patch: Partial<UploadQueueItem>,
|
|
||||||
) => {
|
|
||||||
setUploadQueues((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[sectionKey]: (prev[sectionKey] || []).map((item) =>
|
|
||||||
item.id === itemId ? { ...item, ...patch } : item,
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const markSectionUploading = (sectionKey: string, isUploading: boolean) => {
|
|
||||||
setUploadingSections((prev) => {
|
|
||||||
if (isUploading) {
|
|
||||||
if (prev.includes(sectionKey)) {
|
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
return [...prev, sectionKey];
|
|
||||||
}
|
|
||||||
|
|
||||||
return prev.filter((key) => key !== sectionKey);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleUploadedList = (sectionKey: string) => {
|
|
||||||
setExpandedUploadedLists((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[sectionKey]: !(prev[sectionKey] ?? false),
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const uploadAssetFile = async (
|
|
||||||
section: AssetSection,
|
|
||||||
projectId: string,
|
|
||||||
itemId: string,
|
|
||||||
file: File,
|
|
||||||
) => {
|
|
||||||
updateSectionUpload(section.key, itemId, {
|
|
||||||
status: 'uploading',
|
|
||||||
progress: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const remoteFile = await FileUploader.uploadChunked(
|
|
||||||
`assets/${projectId}`,
|
|
||||||
file,
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
chunkSize: 5 * 1024 * 1024,
|
|
||||||
maxRetries: 3,
|
|
||||||
onProgress: (progress: number) => {
|
|
||||||
updateSectionUpload(section.key, itemId, {
|
|
||||||
progress,
|
|
||||||
status: 'uploading',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onStatus: (status: string) => {
|
|
||||||
if (status === 'finalizing') {
|
|
||||||
updateSectionUpload(section.key, itemId, { status: 'saving' });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await axios.post('/assets', {
|
|
||||||
data: {
|
|
||||||
project: projectId,
|
|
||||||
name: file.name,
|
|
||||||
asset_type: section.assetFormat,
|
|
||||||
type: section.assetCategory,
|
|
||||||
cdn_url: remoteFile.publicUrl,
|
|
||||||
storage_key: remoteFile.privateUrl,
|
|
||||||
mime_type: file.type || null,
|
|
||||||
size_mb: Number((file.size / (1024 * 1024)).toFixed(4)),
|
|
||||||
is_public: false,
|
|
||||||
is_deleted: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
updateSectionUpload(section.key, itemId, {
|
|
||||||
status: 'success',
|
|
||||||
progress: 100,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const runBatchUpload = async (section: AssetSection, files: File[]) => {
|
|
||||||
if (!selectedProjectId) {
|
|
||||||
toast('Select a project first', {
|
|
||||||
type: 'warning',
|
|
||||||
position: 'bottom-center',
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!files.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const queueItems: UploadQueueItem[] = files.map((file) => ({
|
|
||||||
id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
||||||
fileName: file.name,
|
|
||||||
progress: 0,
|
|
||||||
status: 'queued',
|
|
||||||
}));
|
|
||||||
|
|
||||||
addSectionUpload(section.key, queueItems);
|
|
||||||
markSectionUploading(section.key, true);
|
|
||||||
|
|
||||||
const queue = files.map((file, index) => ({
|
|
||||||
file,
|
|
||||||
itemId: queueItems[index].id,
|
|
||||||
}));
|
|
||||||
const maxConcurrent = 2;
|
|
||||||
let currentIndex = 0;
|
|
||||||
let failedCount = 0;
|
|
||||||
|
|
||||||
const worker = async () => {
|
|
||||||
while (currentIndex < queue.length) {
|
|
||||||
const nextIndex = currentIndex;
|
|
||||||
currentIndex += 1;
|
|
||||||
const item = queue[nextIndex];
|
|
||||||
|
|
||||||
try {
|
|
||||||
await uploadAssetFile(
|
|
||||||
section,
|
|
||||||
selectedProjectId,
|
|
||||||
item.itemId,
|
|
||||||
item.file,
|
|
||||||
);
|
|
||||||
} catch (error: any) {
|
|
||||||
const message =
|
|
||||||
error?.response?.data?.message || error?.message || 'Upload failed';
|
|
||||||
console.error(`Failed to upload ${item.file.name}:`, error);
|
|
||||||
failedCount += 1;
|
|
||||||
updateSectionUpload(section.key, item.itemId, {
|
|
||||||
status: 'error',
|
|
||||||
error: message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Promise.all(
|
|
||||||
Array.from({ length: Math.min(maxConcurrent, queue.length) }, () =>
|
|
||||||
worker(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await loadAssets(selectedProjectId);
|
|
||||||
if (failedCount > 0) {
|
|
||||||
toast(
|
|
||||||
`Batch upload finished: ${queue.length - failedCount}/${queue.length} succeeded`,
|
|
||||||
{
|
|
||||||
type: 'warning',
|
|
||||||
position: 'bottom-center',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
toast(`Batch upload finished for ${section.label}`, {
|
|
||||||
type: 'success',
|
|
||||||
position: 'bottom-center',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
markSectionUploading(section.key, false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteAsset = async (assetId: string) => {
|
|
||||||
setDeletingAssetId(assetId);
|
setDeletingAssetId(assetId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await axios.delete(`/assets/${assetId}`);
|
await dispatch(deleteAsset(assetId)).unwrap();
|
||||||
toast('Asset deleted', { type: 'success', position: 'bottom-center' });
|
toast('Asset deleted', { type: 'success', position: 'bottom-center' });
|
||||||
await loadAssets(selectedProjectId);
|
loadAssets(selectedProjectId);
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.error('Failed to delete asset:', error?.message || error);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
console.error('Failed to delete asset:', errorMessage);
|
||||||
toast('Failed to delete asset', {
|
toast('Failed to delete asset', {
|
||||||
type: 'error',
|
type: 'error',
|
||||||
position: 'bottom-center',
|
position: 'bottom-center',
|
||||||
@ -429,7 +122,7 @@ const AssetsTablesPage = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
setDeletingAssetId('');
|
setDeletingAssetId('');
|
||||||
}
|
}
|
||||||
};
|
}, [dispatch, selectedProjectId, loadAssets]);
|
||||||
|
|
||||||
const assetsBySection = useMemo(() => {
|
const assetsBySection = useMemo(() => {
|
||||||
return ASSET_SECTIONS.reduce<Record<string, Asset[]>>((acc, section) => {
|
return ASSET_SECTIONS.reduce<Record<string, Asset[]>>((acc, section) => {
|
||||||
@ -458,143 +151,26 @@ const AssetsTablesPage = () => {
|
|||||||
<p className='mb-6 text-sm font-semibold'>
|
<p className='mb-6 text-sm font-semibold'>
|
||||||
{isLoadingProjects
|
{isLoadingProjects
|
||||||
? 'Loading project...'
|
? 'Loading project...'
|
||||||
: projects.find((project) => project.id === selectedProjectId)
|
: selectedProjectName || 'No project selected'}
|
||||||
?.name || 'No project selected'}
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className='grid grid-cols-1 xl:grid-cols-2 gap-4'>
|
<div className='grid grid-cols-1 xl:grid-cols-2 gap-4'>
|
||||||
{ASSET_SECTIONS.map((section) => {
|
{ASSET_SECTIONS.map((section) => (
|
||||||
const list = assetsBySection[section.key] || [];
|
<AssetSectionCard
|
||||||
const isUploadedListExpanded =
|
key={section.key}
|
||||||
expandedUploadedLists[section.key] ?? false;
|
section={section}
|
||||||
|
assets={assetsBySection[section.key] || []}
|
||||||
return (
|
uploadQueue={uploadQueues[section.key] || []}
|
||||||
<CardBox key={section.key} className='h-full'>
|
isUploading={uploadingSections.includes(section.key)}
|
||||||
<div className='flex items-center justify-between gap-3 mb-4'>
|
isLoadingAssets={isLoadingAssets}
|
||||||
<h3 className='text-lg font-semibold'>{section.label}</h3>
|
hasCreatePermission={hasCreatePermission}
|
||||||
{!hasCreatePermission && (
|
hasDeletePermission={hasDeletePermission}
|
||||||
<p className='text-xs text-gray-500'>
|
deletingAssetId={deletingAssetId}
|
||||||
You do not have upload permission
|
onUpload={(files) => runBatchUpload(section, files)}
|
||||||
</p>
|
onDeleteAsset={handleDeleteAsset}
|
||||||
)}
|
disabled={!selectedProjectId}
|
||||||
</div>
|
/>
|
||||||
<input
|
))}
|
||||||
type='file'
|
|
||||||
multiple
|
|
||||||
accept={section.accept}
|
|
||||||
className='w-full border border-gray-300 rounded px-2 py-2 mb-3 bg-white dark:bg-dark-800'
|
|
||||||
disabled={
|
|
||||||
uploadingSections.includes(section.key) ||
|
|
||||||
!selectedProjectId ||
|
|
||||||
!hasCreatePermission
|
|
||||||
}
|
|
||||||
onChange={(event) => {
|
|
||||||
const files = Array.from(event.target.files || []);
|
|
||||||
void runBatchUpload(section, files);
|
|
||||||
event.currentTarget.value = '';
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{uploadingSections.includes(section.key) && (
|
|
||||||
<p className='text-sm text-gray-500 mb-3'>
|
|
||||||
Uploading {section.label.toLowerCase()} in chunks...
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{(uploadQueues[section.key] || []).filter(
|
|
||||||
(item) => item.status !== 'success',
|
|
||||||
).length > 0 && (
|
|
||||||
<ul className='space-y-1 mb-3 max-h-40 overflow-auto'>
|
|
||||||
{(uploadQueues[section.key] || [])
|
|
||||||
.filter((item) => item.status !== 'success')
|
|
||||||
.map((item) => (
|
|
||||||
<li
|
|
||||||
key={item.id}
|
|
||||||
className='text-xs border border-gray-200 dark:border-dark-700 rounded p-2'
|
|
||||||
>
|
|
||||||
<div className='flex items-center justify-between gap-2'>
|
|
||||||
<span className='truncate'>{item.fileName}</span>
|
|
||||||
<span>
|
|
||||||
{item.status === 'error'
|
|
||||||
? 'Error'
|
|
||||||
: item.status === 'success'
|
|
||||||
? 'Done'
|
|
||||||
: item.status}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{item.status !== 'success' && (
|
|
||||||
<div className='w-full h-1 bg-gray-200 rounded mt-1'>
|
|
||||||
<div
|
|
||||||
className='h-1 bg-blue-500 rounded'
|
|
||||||
style={{ width: `${item.progress}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{item.error && (
|
|
||||||
<p className='text-red-500 mt-1 truncate'>
|
|
||||||
{item.error}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isLoadingAssets && (
|
|
||||||
<p className='text-sm text-gray-500'>Loading assets...</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isLoadingAssets && list.length > 0 && (
|
|
||||||
<button
|
|
||||||
type='button'
|
|
||||||
className='mb-2 text-xs underline'
|
|
||||||
onClick={() => toggleUploadedList(section.key)}
|
|
||||||
>
|
|
||||||
{isUploadedListExpanded
|
|
||||||
? `Hide uploaded ${section.label.toLowerCase()}`
|
|
||||||
: `Show uploaded ${section.label.toLowerCase()} (${list.length})`}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isLoadingAssets && list.length === 0 && (
|
|
||||||
<p className='text-sm text-gray-500'>
|
|
||||||
No uploaded {section.label.toLowerCase()}.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isLoadingAssets &&
|
|
||||||
list.length > 0 &&
|
|
||||||
isUploadedListExpanded && (
|
|
||||||
<ul className='space-y-2'>
|
|
||||||
{list.map((asset) => (
|
|
||||||
<li
|
|
||||||
key={asset.id}
|
|
||||||
className='flex items-center justify-between gap-2 p-2 border border-gray-200 dark:border-dark-700 rounded'
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href={asset.cdn_url || '#'}
|
|
||||||
target='_blank'
|
|
||||||
rel='noreferrer'
|
|
||||||
className='text-sm underline truncate'
|
|
||||||
>
|
|
||||||
{asset.name.replace(/^\[[^\]]+\]\s*/, '')}
|
|
||||||
</a>
|
|
||||||
<BaseButton
|
|
||||||
color='danger'
|
|
||||||
label='X'
|
|
||||||
small
|
|
||||||
disabled={
|
|
||||||
deletingAssetId === asset.id ||
|
|
||||||
!hasDeletePermission
|
|
||||||
}
|
|
||||||
onClick={() => handleDeleteAsset(asset.id)}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</CardBox>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</SectionMain>
|
</SectionMain>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
|
|||||||
@ -10,54 +10,31 @@ import LayoutAuthenticated from '../../layouts/Authenticated';
|
|||||||
import SectionMain from '../../components/SectionMain';
|
import SectionMain from '../../components/SectionMain';
|
||||||
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
|
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
|
||||||
import { getPageTitle } from '../../config';
|
import { getPageTitle } from '../../config';
|
||||||
|
import { useAppDispatch, useAppSelector } from '../../stores/hooks';
|
||||||
type ProjectItem = {
|
import { fetch as fetchProjects, create as createProject } from '../../stores/projects/projectsSlice';
|
||||||
id: string;
|
import type { Project } from '../../types/entities';
|
||||||
name: string;
|
|
||||||
slug: string;
|
|
||||||
phase: string;
|
|
||||||
description?: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ProjectsListPage = () => {
|
const ProjectsListPage = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [projects, setProjects] = useState<ProjectItem[]>([]);
|
const dispatch = useAppDispatch();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
const projects = useAppSelector((state) => state.projects.projects) as Project[];
|
||||||
|
const isLoading = useAppSelector((state) => state.projects.loading);
|
||||||
|
|
||||||
const [isCreating, setIsCreating] = useState(false);
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
const [isCloning, setIsCloning] = useState(false);
|
const [isCloning, setIsCloning] = useState(false);
|
||||||
const [isCloneOpen, setIsCloneOpen] = useState(false);
|
const [isCloneOpen, setIsCloneOpen] = useState(false);
|
||||||
const [cloneSourceId, setCloneSourceId] = useState('');
|
const [cloneSourceId, setCloneSourceId] = useState('');
|
||||||
|
|
||||||
const loadProjects = async () => {
|
useEffect(() => {
|
||||||
setIsLoading(true);
|
dispatch(fetchProjects({ query: '?limit=100&page=0&sort=desc&field=updatedAt' }));
|
||||||
try {
|
}, [dispatch]);
|
||||||
const response = await axios.get(
|
|
||||||
'/projects?limit=100&page=0&sort=desc&field=updatedAt',
|
|
||||||
);
|
|
||||||
const rows = Array.isArray(response?.data?.rows)
|
|
||||||
? response.data.rows
|
|
||||||
: [];
|
|
||||||
setProjects(rows);
|
|
||||||
if (rows.length > 0 && !cloneSourceId) {
|
|
||||||
setCloneSourceId(rows[0].id);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error(
|
|
||||||
'Failed to load projects list:',
|
|
||||||
error?.message || 'Unknown error',
|
|
||||||
);
|
|
||||||
toast('Failed to load projects', {
|
|
||||||
type: 'error',
|
|
||||||
position: 'bottom-center',
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadProjects();
|
if (projects.length > 0 && !cloneSourceId) {
|
||||||
}, []);
|
setCloneSourceId(projects[0].id);
|
||||||
|
}
|
||||||
|
}, [projects, cloneSourceId]);
|
||||||
|
|
||||||
const buildNewProjectDraft = () => {
|
const buildNewProjectDraft = () => {
|
||||||
const stamp = Date.now();
|
const stamp = Date.now();
|
||||||
@ -76,21 +53,17 @@ const ProjectsListPage = () => {
|
|||||||
const handleCreateNewProject = async () => {
|
const handleCreateNewProject = async () => {
|
||||||
setIsCreating(true);
|
setIsCreating(true);
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/projects', {
|
const result = await dispatch(createProject(buildNewProjectDraft())).unwrap();
|
||||||
data: buildNewProjectDraft(),
|
const createdId = result?.id;
|
||||||
});
|
|
||||||
const createdId = response?.data?.id;
|
|
||||||
|
|
||||||
if (!createdId) {
|
if (!createdId) {
|
||||||
throw new Error('Project was created but id is missing in response');
|
throw new Error('Project was created but id is missing in response');
|
||||||
}
|
}
|
||||||
|
|
||||||
await router.push(`/projects/${createdId}`);
|
await router.push(`/projects/${createdId}`);
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.error(
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||||
'Failed to create project:',
|
console.error('Failed to create project:', errorMessage);
|
||||||
error?.message || 'Unknown error',
|
|
||||||
);
|
|
||||||
toast('Failed to create project', {
|
toast('Failed to create project', {
|
||||||
type: 'error',
|
type: 'error',
|
||||||
position: 'bottom-center',
|
position: 'bottom-center',
|
||||||
@ -119,11 +92,9 @@ const ProjectsListPage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await router.push(`/projects/${createdId}`);
|
await router.push(`/projects/${createdId}`);
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
console.error(
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||||
'Failed to clone project:',
|
console.error('Failed to clone project:', errorMessage);
|
||||||
error?.message || 'Unknown error',
|
|
||||||
);
|
|
||||||
toast('Failed to clone project', {
|
toast('Failed to clone project', {
|
||||||
type: 'error',
|
type: 'error',
|
||||||
position: 'bottom-center',
|
position: 'bottom-center',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user