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",
|
||||
"lint": "eslint . --ext .js",
|
||||
"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:undo": "sequelize-cli db:seed:undo:all",
|
||||
"db:drop": "sequelize-cli db:drop",
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@ -19,11 +19,11 @@ class AssetsDBApi extends GenericDBApi {
|
||||
}
|
||||
|
||||
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() {
|
||||
return ['asset_type', 'type', 'is_public', 'is_deleted'];
|
||||
return ['asset_type', 'type', 'is_public'];
|
||||
}
|
||||
|
||||
static get CSV_FIELDS() {
|
||||
@ -68,8 +68,6 @@ class AssetsDBApi extends GenericDBApi {
|
||||
duration_sec: data.duration_sec || null,
|
||||
checksum: data.checksum || null,
|
||||
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,
|
||||
});
|
||||
|
||||
await db.sequelize.transaction(async (tx) => {
|
||||
for (const record of records) {
|
||||
await record.update({ deletedBy: currentUser.id }, { transaction: tx });
|
||||
}
|
||||
for (const record of records) {
|
||||
await record.destroy({ transaction: tx });
|
||||
}
|
||||
});
|
||||
for (const record of records) {
|
||||
await record.update({ deletedBy: currentUser.id }, { transaction });
|
||||
}
|
||||
for (const record of records) {
|
||||
await record.destroy({ transaction });
|
||||
}
|
||||
|
||||
return records;
|
||||
}
|
||||
@ -163,11 +161,14 @@ class GenericDBApi {
|
||||
|
||||
static async findBy(where, options = {}) {
|
||||
const transaction = options.transaction;
|
||||
const include = options.include !== undefined
|
||||
? options.include
|
||||
: this.FIND_BY_INCLUDES;
|
||||
|
||||
const record = await this.MODEL.findOne({
|
||||
where,
|
||||
transaction,
|
||||
include: this.FIND_BY_INCLUDES,
|
||||
include,
|
||||
});
|
||||
|
||||
if (!record) {
|
||||
|
||||
@ -23,11 +23,11 @@ class ProjectsDBApi extends GenericDBApi {
|
||||
}
|
||||
|
||||
static get RANGE_FIELDS() {
|
||||
return ['deleted_at_time'];
|
||||
return [];
|
||||
}
|
||||
|
||||
static get ENUM_FIELDS() {
|
||||
return ['phase', 'is_deleted'];
|
||||
return ['phase'];
|
||||
}
|
||||
|
||||
static get CSV_FIELDS() {
|
||||
@ -56,11 +56,27 @@ class ProjectsDBApi extends GenericDBApi {
|
||||
custom_css_json: data.custom_css_json || null,
|
||||
cdn_base_url: data.cdn_base_url || 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 = {}) {
|
||||
const transaction = options.transaction;
|
||||
const runtimeEnvironment = getRuntimeEnvironment(options);
|
||||
@ -77,20 +93,14 @@ class ProjectsDBApi extends GenericDBApi {
|
||||
queryWhere.slug = runtimeProjectSlug;
|
||||
}
|
||||
|
||||
const include = options.include !== undefined
|
||||
? options.include
|
||||
: this.DEFAULT_INCLUDES;
|
||||
|
||||
const record = await this.MODEL.findOne({
|
||||
where: queryWhere,
|
||||
transaction,
|
||||
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' },
|
||||
],
|
||||
include,
|
||||
});
|
||||
|
||||
if (!record) return null;
|
||||
|
||||
@ -175,7 +175,7 @@ class Ui_elementsDBApi extends GenericDBApi {
|
||||
const now = new Date();
|
||||
await this.MODEL.bulkCreate(
|
||||
this.DEFAULT_ROWS.map((item) => ({
|
||||
...item,
|
||||
...this.getFieldMapping(item),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})),
|
||||
|
||||
@ -10,6 +10,8 @@ module.exports = {
|
||||
port: process.env.DB_PORT,
|
||||
logging: false,
|
||||
seederStorage: 'sequelize',
|
||||
migrationStorage: 'sequelize',
|
||||
migrationStorageTableName: 'SequelizeMeta',
|
||||
},
|
||||
development: {
|
||||
username: 'postgres',
|
||||
@ -19,6 +21,8 @@ module.exports = {
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
logging: console.log,
|
||||
seederStorage: 'sequelize',
|
||||
migrationStorage: 'sequelize',
|
||||
migrationStorageTableName: 'SequelizeMeta',
|
||||
},
|
||||
dev_stage: {
|
||||
dialect: 'postgres',
|
||||
@ -29,5 +33,7 @@ module.exports = {
|
||||
port: process.env.DB_PORT,
|
||||
logging: console.log,
|
||||
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: {
|
||||
name: 'projectId',
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
});
|
||||
|
||||
db.access_logs.belongsTo(db.users, {
|
||||
@ -113,7 +115,9 @@ accessed_at: {
|
||||
foreignKey: {
|
||||
name: 'userId',
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
});
|
||||
|
||||
|
||||
|
||||
@ -114,7 +114,9 @@ size_mb: {
|
||||
foreignKey: {
|
||||
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: {
|
||||
@ -171,7 +154,6 @@ deleted_at_time: {
|
||||
{ fields: ['asset_type'] },
|
||||
{ fields: ['type'] },
|
||||
{ fields: ['is_public'] },
|
||||
{ fields: ['is_deleted'] },
|
||||
{ fields: ['deletedAt'] },
|
||||
],
|
||||
},
|
||||
@ -194,7 +176,9 @@ deleted_at_time: {
|
||||
foreignKey: {
|
||||
name: 'assetId',
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
});
|
||||
|
||||
|
||||
@ -217,7 +201,9 @@ deleted_at_time: {
|
||||
foreignKey: {
|
||||
name: 'projectId',
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
});
|
||||
|
||||
|
||||
|
||||
@ -163,7 +163,9 @@ content_json: {
|
||||
name: 'pageId',
|
||||
allowNull: false,
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
});
|
||||
|
||||
|
||||
|
||||
@ -104,7 +104,9 @@ trigger_selector: {
|
||||
foreignKey: {
|
||||
name: 'from_pageId',
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
});
|
||||
|
||||
db.page_links.belongsTo(db.tour_pages, {
|
||||
@ -112,7 +114,9 @@ trigger_selector: {
|
||||
foreignKey: {
|
||||
name: 'to_pageId',
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'SET NULL',
|
||||
onUpdate: 'CASCADE',
|
||||
});
|
||||
|
||||
db.page_links.belongsTo(db.transitions, {
|
||||
@ -120,7 +124,9 @@ trigger_selector: {
|
||||
foreignKey: {
|
||||
name: 'transitionId',
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'SET NULL',
|
||||
onUpdate: 'CASCADE',
|
||||
});
|
||||
|
||||
|
||||
|
||||
@ -131,7 +131,9 @@ status: {
|
||||
foreignKey: {
|
||||
name: 'projectId',
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
});
|
||||
|
||||
db.presigned_url_requests.belongsTo(db.users, {
|
||||
@ -139,7 +141,9 @@ status: {
|
||||
foreignKey: {
|
||||
name: 'userId',
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
});
|
||||
|
||||
|
||||
|
||||
@ -135,7 +135,9 @@ is_enabled: {
|
||||
foreignKey: {
|
||||
name: 'projectId',
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
});
|
||||
|
||||
|
||||
|
||||
@ -106,7 +106,9 @@ accepted_at: {
|
||||
foreignKey: {
|
||||
name: 'projectId',
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
});
|
||||
|
||||
db.project_memberships.belongsTo(db.users, {
|
||||
@ -114,7 +116,9 @@ accepted_at: {
|
||||
foreignKey: {
|
||||
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: {
|
||||
@ -147,7 +130,9 @@ deleted_at_time: {
|
||||
foreignKey: {
|
||||
name: 'projectId',
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
});
|
||||
|
||||
|
||||
@ -156,7 +141,9 @@ deleted_at_time: {
|
||||
foreignKey: {
|
||||
name: 'projectId',
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
});
|
||||
|
||||
|
||||
@ -166,7 +153,9 @@ deleted_at_time: {
|
||||
foreignKey: {
|
||||
name: 'projectId',
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
});
|
||||
|
||||
|
||||
@ -175,7 +164,9 @@ deleted_at_time: {
|
||||
foreignKey: {
|
||||
name: 'projectId',
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
});
|
||||
|
||||
|
||||
@ -186,7 +177,9 @@ deleted_at_time: {
|
||||
foreignKey: {
|
||||
name: 'projectId',
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
});
|
||||
|
||||
|
||||
@ -195,7 +188,9 @@ deleted_at_time: {
|
||||
foreignKey: {
|
||||
name: 'projectId',
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
});
|
||||
|
||||
|
||||
@ -204,7 +199,9 @@ deleted_at_time: {
|
||||
foreignKey: {
|
||||
name: 'projectId',
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
});
|
||||
|
||||
|
||||
@ -213,7 +210,9 @@ deleted_at_time: {
|
||||
foreignKey: {
|
||||
name: 'projectId',
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
});
|
||||
|
||||
|
||||
@ -222,7 +221,9 @@ deleted_at_time: {
|
||||
foreignKey: {
|
||||
name: 'projectId',
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
});
|
||||
|
||||
|
||||
|
||||
@ -175,7 +175,9 @@ audios_copied: {
|
||||
foreignKey: {
|
||||
name: 'projectId',
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
});
|
||||
|
||||
db.publish_events.belongsTo(db.users, {
|
||||
@ -183,7 +185,9 @@ audios_copied: {
|
||||
foreignKey: {
|
||||
name: 'userId',
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
});
|
||||
|
||||
|
||||
|
||||
@ -104,7 +104,9 @@ is_active: {
|
||||
foreignKey: {
|
||||
name: 'projectId',
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
});
|
||||
|
||||
|
||||
|
||||
@ -44,7 +44,8 @@ role_customization: {
|
||||
foreignKey: {
|
||||
name: 'roles_permissionsId',
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'CASCADE',
|
||||
through: 'rolesPermissionsPermissions',
|
||||
});
|
||||
|
||||
@ -53,7 +54,8 @@ role_customization: {
|
||||
foreignKey: {
|
||||
name: 'roles_permissionsId',
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'CASCADE',
|
||||
through: 'rolesPermissionsPermissions',
|
||||
});
|
||||
|
||||
@ -66,7 +68,9 @@ role_customization: {
|
||||
foreignKey: {
|
||||
name: 'app_roleId',
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'SET NULL',
|
||||
onUpdate: 'CASCADE',
|
||||
});
|
||||
|
||||
|
||||
|
||||
@ -144,7 +144,9 @@ ui_schema_json: {
|
||||
foreignKey: {
|
||||
name: 'pageId',
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
});
|
||||
|
||||
|
||||
@ -153,7 +155,9 @@ ui_schema_json: {
|
||||
foreignKey: {
|
||||
name: 'from_pageId',
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
});
|
||||
|
||||
db.tour_pages.hasMany(db.page_links, {
|
||||
@ -161,7 +165,9 @@ ui_schema_json: {
|
||||
foreignKey: {
|
||||
name: 'to_pageId',
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'SET NULL',
|
||||
onUpdate: 'CASCADE',
|
||||
});
|
||||
|
||||
|
||||
@ -180,7 +186,9 @@ ui_schema_json: {
|
||||
foreignKey: {
|
||||
name: 'projectId',
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
});
|
||||
|
||||
|
||||
|
||||
@ -123,7 +123,9 @@ duration_sec: {
|
||||
foreignKey: {
|
||||
name: 'transitionId',
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'SET NULL',
|
||||
onUpdate: 'CASCADE',
|
||||
});
|
||||
|
||||
|
||||
@ -142,7 +144,9 @@ duration_sec: {
|
||||
foreignKey: {
|
||||
name: 'projectId',
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
});
|
||||
|
||||
|
||||
|
||||
@ -130,7 +130,8 @@ provider: {
|
||||
foreignKey: {
|
||||
name: 'users_custom_permissionsId',
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'CASCADE',
|
||||
through: 'usersCustom_permissionsPermissions',
|
||||
});
|
||||
|
||||
@ -139,7 +140,8 @@ provider: {
|
||||
foreignKey: {
|
||||
name: 'users_custom_permissionsId',
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'CASCADE',
|
||||
through: 'usersCustom_permissionsPermissions',
|
||||
});
|
||||
|
||||
@ -156,7 +158,9 @@ provider: {
|
||||
foreignKey: {
|
||||
name: 'userId',
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
});
|
||||
|
||||
|
||||
@ -167,7 +171,9 @@ provider: {
|
||||
foreignKey: {
|
||||
name: 'userId',
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
});
|
||||
|
||||
|
||||
@ -181,7 +187,9 @@ provider: {
|
||||
foreignKey: {
|
||||
name: 'userId',
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'SET NULL',
|
||||
onUpdate: 'CASCADE',
|
||||
});
|
||||
|
||||
|
||||
@ -191,7 +199,9 @@ provider: {
|
||||
foreignKey: {
|
||||
name: 'userId',
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'SET NULL',
|
||||
onUpdate: 'CASCADE',
|
||||
});
|
||||
|
||||
|
||||
@ -205,7 +215,9 @@ provider: {
|
||||
foreignKey: {
|
||||
name: 'app_roleId',
|
||||
},
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'SET NULL',
|
||||
onUpdate: 'CASCADE',
|
||||
});
|
||||
|
||||
|
||||
@ -213,7 +225,9 @@ provider: {
|
||||
db.users.hasMany(db.file, {
|
||||
as: 'avatar',
|
||||
foreignKey: 'belongsToId',
|
||||
constraints: false,
|
||||
constraints: true,
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE',
|
||||
scope: {
|
||||
belongsTo: db.users.getTableName(),
|
||||
belongsToColumn: 'avatar',
|
||||
|
||||
@ -1,9 +1,14 @@
|
||||
const db = require('./models');
|
||||
|
||||
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 {
|
||||
console.log('Syncing database...');
|
||||
await db.sequelize.sync({ force: true });
|
||||
await db.sequelize.sync({ alter: true });
|
||||
console.log('Database synced successfully!');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
const express = require('express');
|
||||
const { wrapAsync, commonErrorHandler } = require('../helpers');
|
||||
const { wrapAsync, commonErrorHandler, isUuidV4 } = require('../helpers');
|
||||
const { checkCrudPermissions } = require('../middlewares/check-permissions');
|
||||
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 = {}) {
|
||||
const router = express.Router();
|
||||
|
||||
@ -70,14 +67,14 @@ function createEntityRouter(entityName, Service, DBApi, options = {}) {
|
||||
res.status(200).send(payload);
|
||||
}));
|
||||
|
||||
router.get('/autocomplete', async (req, res) => {
|
||||
router.get('/autocomplete', wrapAsync(async (req, res) => {
|
||||
const payload = await DBApi.findAllAutocomplete(
|
||||
req.query.query,
|
||||
req.query.limit,
|
||||
req.query.offset
|
||||
);
|
||||
res.status(200).send(payload);
|
||||
});
|
||||
}));
|
||||
|
||||
router.get('/:id', wrapAsync(async (req, res) => {
|
||||
if (!isUuidV4(req.params.id)) {
|
||||
|
||||
@ -22,4 +22,8 @@ module.exports = class Helpers {
|
||||
static jwtSign(data) {
|
||||
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 ProjectsDBApi = require('../db/api/projects');
|
||||
const wrapAsync = require('../helpers').wrapAsync;
|
||||
const { wrapAsync, isUuidV4 } = require('../helpers');
|
||||
|
||||
|
||||
const router = express.Router();
|
||||
@ -17,9 +17,6 @@ const {
|
||||
|
||||
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
|
||||
@ -328,11 +325,7 @@ router.get('/', wrapAsync(async (req, res) => {
|
||||
req.query, { currentUser, runtimeContext }
|
||||
);
|
||||
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',
|
||||
|
||||
|
||||
'deleted_at_time',
|
||||
];
|
||||
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 opts = { fields };
|
||||
try {
|
||||
const csv = parse(payload.rows, opts);
|
||||
|
||||
@ -1,136 +1,6 @@
|
||||
const db = require('../db/models');
|
||||
const Access_logsDBApi = require('../db/api/access_logs');
|
||||
const processFile = require("../middlewares/upload");
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
const { createEntityService } = require('../factories/service.factory');
|
||||
|
||||
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 processFile = require("../middlewares/upload");
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
const { createEntityService } = require('../factories/service.factory');
|
||||
|
||||
module.exports = createEntityService(Asset_variantsDBApi, {
|
||||
entityName: 'asset_variants',
|
||||
});
|
||||
|
||||
@ -1,136 +1,6 @@
|
||||
const db = require('../db/models');
|
||||
const AssetsDBApi = require('../db/api/assets');
|
||||
const processFile = require("../middlewares/upload");
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
const { createEntityService } = require('../factories/service.factory');
|
||||
|
||||
module.exports = createEntityService(AssetsDBApi, {
|
||||
entityName: 'assets',
|
||||
});
|
||||
|
||||
@ -186,6 +186,7 @@ const downloadLocal = async (req, res) => {
|
||||
if (!privateUrl) {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
|
||||
res.download(path.join(config.uploadDir, privateUrl));
|
||||
}
|
||||
|
||||
@ -348,6 +349,7 @@ const downloadGCloud = async (req, res) => {
|
||||
const fileExists = await file.exists();
|
||||
|
||||
if (fileExists[0]) {
|
||||
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
|
||||
const stream = file.createReadStream();
|
||||
stream.pipe(res);
|
||||
}
|
||||
@ -428,6 +430,7 @@ const downloadS3 = async (req, res) => {
|
||||
if (output.ContentType) {
|
||||
res.setHeader('Content-Type', output.ContentType);
|
||||
}
|
||||
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
|
||||
|
||||
if (typeof output.Body.pipe === 'function') {
|
||||
output.Body.pipe(res);
|
||||
|
||||
@ -1,136 +1,6 @@
|
||||
const db = require('../db/models');
|
||||
const Page_elementsDBApi = require('../db/api/page_elements');
|
||||
const processFile = require("../middlewares/upload");
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
const { createEntityService } = require('../factories/service.factory');
|
||||
|
||||
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 processFile = require("../middlewares/upload");
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
const { createEntityService } = require('../factories/service.factory');
|
||||
|
||||
module.exports = createEntityService(Page_linksDBApi, {
|
||||
entityName: 'page_links',
|
||||
});
|
||||
|
||||
@ -1,136 +1,6 @@
|
||||
const db = require('../db/models');
|
||||
const PermissionsDBApi = require('../db/api/permissions');
|
||||
const processFile = require("../middlewares/upload");
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
const { createEntityService } = require('../factories/service.factory');
|
||||
|
||||
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 processFile = require("../middlewares/upload");
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
const { createEntityService } = require('../factories/service.factory');
|
||||
|
||||
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) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
try {
|
||||
await Project_audio_tracksDBApi.create(
|
||||
const createdTrack = await Project_audio_tracksDBApi.create(
|
||||
data,
|
||||
{
|
||||
currentUser,
|
||||
@ -22,6 +22,7 @@ module.exports = class Project_audio_tracksService {
|
||||
);
|
||||
|
||||
await transaction.commit();
|
||||
return createdTrack;
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
|
||||
@ -1,136 +1,6 @@
|
||||
const db = require('../db/models');
|
||||
const Project_membershipsDBApi = require('../db/api/project_memberships');
|
||||
const processFile = require("../middlewares/upload");
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
const { createEntityService } = require('../factories/service.factory');
|
||||
|
||||
module.exports = createEntityService(Project_membershipsDBApi, {
|
||||
entityName: 'project_memberships',
|
||||
});
|
||||
|
||||
@ -106,8 +106,6 @@ module.exports = class ProjectsService {
|
||||
custom_css_json: sourceProject.custom_css_json,
|
||||
cdn_base_url: sourceProject.cdn_base_url,
|
||||
entry_page_slug: sourceProject.entry_page_slug,
|
||||
is_deleted: false,
|
||||
deleted_at_time: null,
|
||||
},
|
||||
{
|
||||
currentUser,
|
||||
@ -130,8 +128,6 @@ module.exports = class ProjectsService {
|
||||
duration_sec: sourceAsset.duration_sec,
|
||||
checksum: sourceAsset.checksum,
|
||||
is_public: sourceAsset.is_public,
|
||||
is_deleted: false,
|
||||
deleted_at_time: null,
|
||||
projectId: clonedProject.id,
|
||||
createdById: currentUser.id,
|
||||
updatedById: currentUser.id,
|
||||
|
||||
@ -1,136 +1,6 @@
|
||||
const db = require('../db/models');
|
||||
const Publish_eventsDBApi = require('../db/api/publish_events');
|
||||
const processFile = require("../middlewares/upload");
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
const { createEntityService } = require('../factories/service.factory');
|
||||
|
||||
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 processFile = require("../middlewares/upload");
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
const { createEntityService } = require('../factories/service.factory');
|
||||
|
||||
module.exports = createEntityService(Pwa_cachesDBApi, {
|
||||
entityName: 'pwa_caches',
|
||||
});
|
||||
|
||||
@ -59,7 +59,7 @@ module.exports = class RolesService {
|
||||
static async create(data, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
try {
|
||||
await RolesDBApi.create(
|
||||
const createdRole = await RolesDBApi.create(
|
||||
data,
|
||||
{
|
||||
currentUser,
|
||||
@ -68,6 +68,7 @@ module.exports = class RolesService {
|
||||
);
|
||||
|
||||
await transaction.commit();
|
||||
return createdRole;
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
|
||||
@ -1,136 +1,6 @@
|
||||
const db = require('../db/models');
|
||||
const Tour_pagesDBApi = require('../db/api/tour_pages');
|
||||
const processFile = require("../middlewares/upload");
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
const { createEntityService } = require('../factories/service.factory');
|
||||
|
||||
module.exports = createEntityService(Tour_pagesDBApi, {
|
||||
entityName: 'tour_pages',
|
||||
});
|
||||
|
||||
@ -1,136 +1,6 @@
|
||||
const db = require('../db/models');
|
||||
const TransitionsDBApi = require('../db/api/transitions');
|
||||
const processFile = require("../middlewares/upload");
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
const { createEntityService } = require('../factories/service.factory');
|
||||
|
||||
module.exports = createEntityService(TransitionsDBApi, {
|
||||
entityName: 'transitions',
|
||||
});
|
||||
|
||||
@ -2,11 +2,7 @@ import React from 'react';
|
||||
import BaseIcon from '../BaseIcon';
|
||||
import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js';
|
||||
import axios from 'axios';
|
||||
import {
|
||||
GridActionsCellItem,
|
||||
GridRowParams,
|
||||
GridValueGetterParams,
|
||||
} from '@mui/x-data-grid';
|
||||
import { GridActionsCellItem, GridRowParams } from '@mui/x-data-grid';
|
||||
import ImageField from '../ImageField';
|
||||
import { saveFile } from '../../helpers/fileSaver';
|
||||
import dataFormatter from '../../helpers/dataFormatter';
|
||||
@ -54,8 +50,7 @@ export const loadColumns = async (
|
||||
getOptionValue: (value: any) => value?.id,
|
||||
getOptionLabel: (value: any) => value?.label,
|
||||
valueOptions: await callOptionsApi('projects'),
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
params?.value?.id ?? params?.value,
|
||||
valueGetter: (value) => value?.id ?? value,
|
||||
},
|
||||
|
||||
{
|
||||
@ -86,8 +81,7 @@ export const loadColumns = async (
|
||||
getOptionValue: (value: any) => value?.id,
|
||||
getOptionLabel: (value: any) => value?.label,
|
||||
valueOptions: await callOptionsApi('users'),
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
params?.value?.id ?? params?.value,
|
||||
valueGetter: (value) => value?.id ?? value,
|
||||
},
|
||||
|
||||
{
|
||||
@ -138,8 +132,7 @@ export const loadColumns = async (
|
||||
editable: hasUpdatePermission,
|
||||
|
||||
type: 'dateTime',
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
new Date(params.row.accessed_at),
|
||||
valueGetter: (_value, row) => new Date(row.accessed_at),
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
@ -2,11 +2,7 @@ import React from 'react';
|
||||
import BaseIcon from '../BaseIcon';
|
||||
import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js';
|
||||
import axios from 'axios';
|
||||
import {
|
||||
GridActionsCellItem,
|
||||
GridRowParams,
|
||||
GridValueGetterParams,
|
||||
} from '@mui/x-data-grid';
|
||||
import { GridActionsCellItem, GridRowParams } from '@mui/x-data-grid';
|
||||
import ImageField from '../ImageField';
|
||||
import { saveFile } from '../../helpers/fileSaver';
|
||||
import dataFormatter from '../../helpers/dataFormatter';
|
||||
@ -54,8 +50,7 @@ export const loadColumns = async (
|
||||
getOptionValue: (value: any) => value?.id,
|
||||
getOptionLabel: (value: any) => value?.label,
|
||||
valueOptions: await callOptionsApi('assets'),
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
params?.value?.id ?? params?.value,
|
||||
valueGetter: (value) => value?.id ?? 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 { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js';
|
||||
import axios from 'axios';
|
||||
import {
|
||||
GridActionsCellItem,
|
||||
GridRowParams,
|
||||
GridValueGetterParams,
|
||||
} from '@mui/x-data-grid';
|
||||
import { GridActionsCellItem, GridRowParams } from '@mui/x-data-grid';
|
||||
import ImageField from '../ImageField';
|
||||
import { saveFile } from '../../helpers/fileSaver';
|
||||
import dataFormatter from '../../helpers/dataFormatter';
|
||||
@ -54,8 +50,7 @@ export const loadColumns = async (
|
||||
getOptionValue: (value: any) => value?.id,
|
||||
getOptionLabel: (value: any) => value?.label,
|
||||
valueOptions: await callOptionsApi('projects'),
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
params?.value?.id ?? params?.value,
|
||||
valueGetter: (value) => value?.id ?? value,
|
||||
},
|
||||
|
||||
{
|
||||
@ -238,8 +233,7 @@ export const loadColumns = async (
|
||||
editable: hasUpdatePermission,
|
||||
|
||||
type: 'dateTime',
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
new Date(params.row.deleted_at_time),
|
||||
valueGetter: (_value, row) => new Date(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 {
|
||||
GridActionsCellItem,
|
||||
GridRowParams,
|
||||
GridValueGetterParams,
|
||||
|
||||
} from '@mui/x-data-grid';
|
||||
import ImageField from '../ImageField';
|
||||
import { saveFile } from '../../helpers/fileSaver';
|
||||
@ -54,8 +54,7 @@ export const loadColumns = async (
|
||||
getOptionValue: (value: any) => value?.id,
|
||||
getOptionLabel: (value: any) => value?.label,
|
||||
valueOptions: await callOptionsApi('tour_pages'),
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
params?.value?.id ?? params?.value,
|
||||
valueGetter: (value) => value?.id ?? value,
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
@ -5,7 +5,7 @@ import axios from 'axios';
|
||||
import {
|
||||
GridActionsCellItem,
|
||||
GridRowParams,
|
||||
GridValueGetterParams,
|
||||
|
||||
} from '@mui/x-data-grid';
|
||||
import ImageField from '../ImageField';
|
||||
import { saveFile } from '../../helpers/fileSaver';
|
||||
@ -54,8 +54,7 @@ export const loadColumns = async (
|
||||
getOptionValue: (value: any) => value?.id,
|
||||
getOptionLabel: (value: any) => value?.label,
|
||||
valueOptions: await callOptionsApi('tour_pages'),
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
params?.value?.id ?? params?.value,
|
||||
valueGetter: (value) => value?.id ?? value,
|
||||
},
|
||||
|
||||
{
|
||||
@ -74,8 +73,7 @@ export const loadColumns = async (
|
||||
getOptionValue: (value: any) => value?.id,
|
||||
getOptionLabel: (value: any) => value?.label,
|
||||
valueOptions: await callOptionsApi('tour_pages'),
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
params?.value?.id ?? params?.value,
|
||||
valueGetter: (value) => value?.id ?? value,
|
||||
},
|
||||
|
||||
{
|
||||
@ -118,8 +116,7 @@ export const loadColumns = async (
|
||||
getOptionValue: (value: any) => value?.id,
|
||||
getOptionLabel: (value: any) => value?.label,
|
||||
valueOptions: await callOptionsApi('transitions'),
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
params?.value?.id ?? params?.value,
|
||||
valueGetter: (value) => value?.id ?? value,
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
@ -5,7 +5,7 @@ import axios from 'axios';
|
||||
import {
|
||||
GridActionsCellItem,
|
||||
GridRowParams,
|
||||
GridValueGetterParams,
|
||||
|
||||
} from '@mui/x-data-grid';
|
||||
import ImageField from '../ImageField';
|
||||
import { saveFile } from '../../helpers/fileSaver';
|
||||
|
||||
@ -5,7 +5,7 @@ import axios from 'axios';
|
||||
import {
|
||||
GridActionsCellItem,
|
||||
GridRowParams,
|
||||
GridValueGetterParams,
|
||||
|
||||
} from '@mui/x-data-grid';
|
||||
import ImageField from '../ImageField';
|
||||
import { saveFile } from '../../helpers/fileSaver';
|
||||
@ -57,8 +57,7 @@ export const loadColumns = async (
|
||||
getOptionValue: (value: any) => value?.id,
|
||||
getOptionLabel: (value: any) => value?.label,
|
||||
valueOptions: await callOptionsApi('projects'),
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
params?.value?.id ?? params?.value,
|
||||
valueGetter: (value) => value?.id ?? value,
|
||||
},
|
||||
|
||||
{
|
||||
@ -77,8 +76,7 @@ export const loadColumns = async (
|
||||
getOptionValue: (value: any) => value?.id,
|
||||
getOptionLabel: (value: any) => value?.label,
|
||||
valueOptions: await callOptionsApi('users'),
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
params?.value?.id ?? params?.value,
|
||||
valueGetter: (value) => value?.id ?? value,
|
||||
},
|
||||
|
||||
{
|
||||
@ -155,8 +153,7 @@ export const loadColumns = async (
|
||||
editable: hasUpdatePermission,
|
||||
|
||||
type: 'dateTime',
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
new Date(params.row.expires_at),
|
||||
valueGetter: (_value, row) => new Date(row.expires_at),
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
@ -5,7 +5,7 @@ import axios from 'axios';
|
||||
import {
|
||||
GridActionsCellItem,
|
||||
GridRowParams,
|
||||
GridValueGetterParams,
|
||||
|
||||
} from '@mui/x-data-grid';
|
||||
import ImageField from '../ImageField';
|
||||
import { saveFile } from '../../helpers/fileSaver';
|
||||
@ -57,8 +57,7 @@ export const loadColumns = async (
|
||||
getOptionValue: (value: any) => value?.id,
|
||||
getOptionLabel: (value: any) => value?.label,
|
||||
valueOptions: await callOptionsApi('projects'),
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
params?.value?.id ?? params?.value,
|
||||
valueGetter: (value) => value?.id ?? value,
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
@ -5,7 +5,7 @@ import axios from 'axios';
|
||||
import {
|
||||
GridActionsCellItem,
|
||||
GridRowParams,
|
||||
GridValueGetterParams,
|
||||
|
||||
} from '@mui/x-data-grid';
|
||||
import ImageField from '../ImageField';
|
||||
import { saveFile } from '../../helpers/fileSaver';
|
||||
@ -54,8 +54,7 @@ export const loadColumns = async (
|
||||
getOptionValue: (value: any) => value?.id,
|
||||
getOptionLabel: (value: any) => value?.label,
|
||||
valueOptions: await callOptionsApi('projects'),
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
params?.value?.id ?? params?.value,
|
||||
valueGetter: (value) => value?.id ?? value,
|
||||
},
|
||||
|
||||
{
|
||||
@ -74,8 +73,7 @@ export const loadColumns = async (
|
||||
getOptionValue: (value: any) => value?.id,
|
||||
getOptionLabel: (value: any) => value?.label,
|
||||
valueOptions: await callOptionsApi('users'),
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
params?.value?.id ?? params?.value,
|
||||
valueGetter: (value) => value?.id ?? value,
|
||||
},
|
||||
|
||||
{
|
||||
@ -116,8 +114,7 @@ export const loadColumns = async (
|
||||
editable: hasUpdatePermission,
|
||||
|
||||
type: 'dateTime',
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
new Date(params.row.invited_at),
|
||||
valueGetter: (_value, row) => new Date(row.invited_at),
|
||||
},
|
||||
|
||||
{
|
||||
@ -132,8 +129,7 @@ export const loadColumns = async (
|
||||
editable: hasUpdatePermission,
|
||||
|
||||
type: 'dateTime',
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
new Date(params.row.accepted_at),
|
||||
valueGetter: (_value, row) => new Date(row.accepted_at),
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
@ -5,7 +5,7 @@ import axios from 'axios';
|
||||
import {
|
||||
GridActionsCellItem,
|
||||
GridRowParams,
|
||||
GridValueGetterParams,
|
||||
|
||||
} from '@mui/x-data-grid';
|
||||
import ImageField from '../ImageField';
|
||||
import { saveFile } from '../../helpers/fileSaver';
|
||||
@ -196,8 +196,7 @@ export const loadColumns = async (
|
||||
editable: hasUpdatePermission,
|
||||
|
||||
type: 'dateTime',
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
new Date(params.row.deleted_at_time),
|
||||
valueGetter: (_value, row) => new Date(row.deleted_at_time),
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
@ -5,7 +5,7 @@ import axios from 'axios';
|
||||
import {
|
||||
GridActionsCellItem,
|
||||
GridRowParams,
|
||||
GridValueGetterParams,
|
||||
|
||||
} from '@mui/x-data-grid';
|
||||
import ImageField from '../ImageField';
|
||||
import { saveFile } from '../../helpers/fileSaver';
|
||||
@ -54,8 +54,7 @@ export const loadColumns = async (
|
||||
getOptionValue: (value: any) => value?.id,
|
||||
getOptionLabel: (value: any) => value?.label,
|
||||
valueOptions: await callOptionsApi('projects'),
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
params?.value?.id ?? params?.value,
|
||||
valueGetter: (value) => value?.id ?? value,
|
||||
},
|
||||
|
||||
{
|
||||
@ -74,8 +73,7 @@ export const loadColumns = async (
|
||||
getOptionValue: (value: any) => value?.id,
|
||||
getOptionLabel: (value: any) => value?.label,
|
||||
valueOptions: await callOptionsApi('users'),
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
params?.value?.id ?? params?.value,
|
||||
valueGetter: (value) => value?.id ?? value,
|
||||
},
|
||||
|
||||
{
|
||||
@ -114,8 +112,7 @@ export const loadColumns = async (
|
||||
editable: hasUpdatePermission,
|
||||
|
||||
type: 'dateTime',
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
new Date(params.row.started_at),
|
||||
valueGetter: (_value, row) => new Date(row.started_at),
|
||||
},
|
||||
|
||||
{
|
||||
@ -130,8 +127,7 @@ export const loadColumns = async (
|
||||
editable: hasUpdatePermission,
|
||||
|
||||
type: 'dateTime',
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
new Date(params.row.finished_at),
|
||||
valueGetter: (_value, row) => new Date(row.finished_at),
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
@ -5,7 +5,7 @@ import axios from 'axios';
|
||||
import {
|
||||
GridActionsCellItem,
|
||||
GridRowParams,
|
||||
GridValueGetterParams,
|
||||
|
||||
} from '@mui/x-data-grid';
|
||||
import ImageField from '../ImageField';
|
||||
import { saveFile } from '../../helpers/fileSaver';
|
||||
@ -54,8 +54,7 @@ export const loadColumns = async (
|
||||
getOptionValue: (value: any) => value?.id,
|
||||
getOptionLabel: (value: any) => value?.label,
|
||||
valueOptions: await callOptionsApi('projects'),
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
params?.value?.id ?? params?.value,
|
||||
valueGetter: (value) => value?.id ?? value,
|
||||
},
|
||||
|
||||
{
|
||||
@ -118,8 +117,7 @@ export const loadColumns = async (
|
||||
editable: hasUpdatePermission,
|
||||
|
||||
type: 'dateTime',
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
new Date(params.row.generated_at),
|
||||
valueGetter: (_value, row) => new Date(row.generated_at),
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
@ -5,7 +5,7 @@ import axios from 'axios';
|
||||
import {
|
||||
GridActionsCellItem,
|
||||
GridRowParams,
|
||||
GridValueGetterParams,
|
||||
|
||||
} from '@mui/x-data-grid';
|
||||
import ImageField from '../ImageField';
|
||||
import { saveFile } from '../../helpers/fileSaver';
|
||||
|
||||
@ -5,7 +5,7 @@ import axios from 'axios';
|
||||
import {
|
||||
GridActionsCellItem,
|
||||
GridRowParams,
|
||||
GridValueGetterParams,
|
||||
|
||||
} from '@mui/x-data-grid';
|
||||
import ImageField from '../ImageField';
|
||||
import { saveFile } from '../../helpers/fileSaver';
|
||||
@ -54,8 +54,7 @@ export const loadColumns = async (
|
||||
getOptionValue: (value: any) => value?.id,
|
||||
getOptionLabel: (value: any) => value?.label,
|
||||
valueOptions: await callOptionsApi('projects'),
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
params?.value?.id ?? params?.value,
|
||||
valueGetter: (value) => value?.id ?? value,
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
@ -5,7 +5,7 @@ import axios from 'axios';
|
||||
import {
|
||||
GridActionsCellItem,
|
||||
GridRowParams,
|
||||
GridValueGetterParams,
|
||||
|
||||
} from '@mui/x-data-grid';
|
||||
import ImageField from '../ImageField';
|
||||
import { saveFile } from '../../helpers/fileSaver';
|
||||
@ -54,8 +54,7 @@ export const loadColumns = async (
|
||||
getOptionValue: (value: any) => value?.id,
|
||||
getOptionLabel: (value: any) => value?.label,
|
||||
valueOptions: await callOptionsApi('projects'),
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
params?.value?.id ?? params?.value,
|
||||
valueGetter: (value) => value?.id ?? value,
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
@ -75,19 +75,28 @@ export default class FileUploader {
|
||||
typeof options.onProgress === 'function' ? options.onProgress : null;
|
||||
const onStatus =
|
||||
typeof options.onStatus === 'function' ? options.onStatus : null;
|
||||
const signal = options.signal || null;
|
||||
const extension = extractExtensionFrom(file.name);
|
||||
const id = uuidv4();
|
||||
const filename = extension ? `${id}.${extension}` : id;
|
||||
const privateUrl = `${path}/${filename}`;
|
||||
const totalChunks = Math.max(1, Math.ceil(file.size / chunkSize));
|
||||
|
||||
const initResponse = await Axios.post('/file/upload-sessions/init', {
|
||||
folder: path,
|
||||
filename,
|
||||
size: file.size,
|
||||
contentType: file.type || '',
|
||||
totalChunks,
|
||||
});
|
||||
if (signal?.aborted) {
|
||||
throw new Error('Upload aborted');
|
||||
}
|
||||
|
||||
const initResponse = await Axios.post(
|
||||
'/file/upload-sessions/init',
|
||||
{
|
||||
folder: path,
|
||||
filename,
|
||||
size: file.size,
|
||||
contentType: file.type || '',
|
||||
totalChunks,
|
||||
},
|
||||
{ signal },
|
||||
);
|
||||
|
||||
const sessionId = initResponse?.data?.sessionId;
|
||||
|
||||
@ -96,6 +105,10 @@ export default class FileUploader {
|
||||
}
|
||||
|
||||
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex += 1) {
|
||||
if (signal?.aborted) {
|
||||
throw new Error('Upload aborted');
|
||||
}
|
||||
|
||||
const start = chunkIndex * chunkSize;
|
||||
const end = Math.min(file.size, start + chunkSize);
|
||||
const chunk = file.slice(start, end);
|
||||
@ -118,10 +131,14 @@ export default class FileUploader {
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
},
|
||||
signal,
|
||||
},
|
||||
);
|
||||
break;
|
||||
} catch (error) {
|
||||
if (signal?.aborted) {
|
||||
throw new Error('Upload aborted');
|
||||
}
|
||||
retry += 1;
|
||||
if (retry > maxRetries) {
|
||||
throw error;
|
||||
@ -139,12 +156,18 @@ export default class FileUploader {
|
||||
}
|
||||
}
|
||||
|
||||
if (signal?.aborted) {
|
||||
throw new Error('Upload aborted');
|
||||
}
|
||||
|
||||
if (onStatus) {
|
||||
onStatus('finalizing', null);
|
||||
}
|
||||
|
||||
const finalizeResponse = await Axios.post(
|
||||
`/file/upload-sessions/${sessionId}/finalize`,
|
||||
null,
|
||||
{ signal },
|
||||
);
|
||||
const responsePublicUrl = finalizeResponse?.data?.url;
|
||||
const publicUrl = responsePublicUrl
|
||||
|
||||
@ -5,7 +5,7 @@ import axios from 'axios';
|
||||
import {
|
||||
GridActionsCellItem,
|
||||
GridRowParams,
|
||||
GridValueGetterParams,
|
||||
|
||||
} from '@mui/x-data-grid';
|
||||
import ImageField from '../ImageField';
|
||||
import { saveFile } from '../../helpers/fileSaver';
|
||||
@ -111,7 +111,7 @@ export const loadColumns = async (
|
||||
|
||||
editable: false,
|
||||
sortable: false,
|
||||
renderCell: (params: GridValueGetterParams) => (
|
||||
renderCell: (value) => (
|
||||
<ImageField
|
||||
name={'Avatar'}
|
||||
image={params?.row?.avatar}
|
||||
@ -136,8 +136,7 @@ export const loadColumns = async (
|
||||
getOptionValue: (value: any) => value?.id,
|
||||
getOptionLabel: (value: any) => value?.label,
|
||||
valueOptions: await callOptionsApi('roles'),
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
params?.value?.id ?? params?.value,
|
||||
valueGetter: (value) => value?.id ?? value,
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
@ -1,64 +1,17 @@
|
||||
import { mdiChartTimelineVariant } from '@mdi/js';
|
||||
import axios from 'axios';
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { ReactElement, useEffect, useMemo, useState } from 'react';
|
||||
import React, { ReactElement, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
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 SectionMain from '../../components/SectionMain';
|
||||
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
|
||||
import { getPageTitle } from '../../config';
|
||||
import { hasPermission } from '../../helpers/userPermissions';
|
||||
import { useAppSelector } from '../../stores/hooks';
|
||||
|
||||
type Project = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
import { useAppDispatch, useAppSelector } from '../../stores/hooks';
|
||||
import { fetch as fetchAssets, deleteItem as deleteAsset } from '../../stores/assets/assetsSlice';
|
||||
import AssetSectionCard, { Asset, AssetSection } from '../../components/Assets/AssetSectionCard';
|
||||
import { useProjectSelector } from '../../components/Assets/ProjectSelector';
|
||||
import { useAssetUploader } from '../../components/Assets/useAssetUploader';
|
||||
|
||||
const ASSET_SECTIONS: AssetSection[] = [
|
||||
{
|
||||
@ -112,29 +65,39 @@ const ASSET_SECTIONS: AssetSection[] = [
|
||||
];
|
||||
|
||||
const AssetsTablesPage = () => {
|
||||
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 dispatch = useAppDispatch();
|
||||
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 {
|
||||
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(
|
||||
currentUser && hasPermission(currentUser, 'CREATE_ASSETS'),
|
||||
);
|
||||
@ -142,286 +105,16 @@ const AssetsTablesPage = () => {
|
||||
currentUser && hasPermission(currentUser, 'DELETE_ASSETS'),
|
||||
);
|
||||
|
||||
const loadProjects = 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,
|
||||
}));
|
||||
}
|
||||
|
||||
// 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) => {
|
||||
const handleDeleteAsset = useCallback(async (assetId: string) => {
|
||||
setDeletingAssetId(assetId);
|
||||
|
||||
try {
|
||||
await axios.delete(`/assets/${assetId}`);
|
||||
await dispatch(deleteAsset(assetId)).unwrap();
|
||||
toast('Asset deleted', { type: 'success', position: 'bottom-center' });
|
||||
await loadAssets(selectedProjectId);
|
||||
} catch (error: any) {
|
||||
console.error('Failed to delete asset:', error?.message || error);
|
||||
loadAssets(selectedProjectId);
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error('Failed to delete asset:', errorMessage);
|
||||
toast('Failed to delete asset', {
|
||||
type: 'error',
|
||||
position: 'bottom-center',
|
||||
@ -429,7 +122,7 @@ const AssetsTablesPage = () => {
|
||||
} finally {
|
||||
setDeletingAssetId('');
|
||||
}
|
||||
};
|
||||
}, [dispatch, selectedProjectId, loadAssets]);
|
||||
|
||||
const assetsBySection = useMemo(() => {
|
||||
return ASSET_SECTIONS.reduce<Record<string, Asset[]>>((acc, section) => {
|
||||
@ -458,143 +151,26 @@ const AssetsTablesPage = () => {
|
||||
<p className='mb-6 text-sm font-semibold'>
|
||||
{isLoadingProjects
|
||||
? 'Loading project...'
|
||||
: projects.find((project) => project.id === selectedProjectId)
|
||||
?.name || 'No project selected'}
|
||||
: selectedProjectName || 'No project selected'}
|
||||
</p>
|
||||
|
||||
<div className='grid grid-cols-1 xl:grid-cols-2 gap-4'>
|
||||
{ASSET_SECTIONS.map((section) => {
|
||||
const list = assetsBySection[section.key] || [];
|
||||
const isUploadedListExpanded =
|
||||
expandedUploadedLists[section.key] ?? false;
|
||||
|
||||
return (
|
||||
<CardBox key={section.key} 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={
|
||||
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>
|
||||
);
|
||||
})}
|
||||
{ASSET_SECTIONS.map((section) => (
|
||||
<AssetSectionCard
|
||||
key={section.key}
|
||||
section={section}
|
||||
assets={assetsBySection[section.key] || []}
|
||||
uploadQueue={uploadQueues[section.key] || []}
|
||||
isUploading={uploadingSections.includes(section.key)}
|
||||
isLoadingAssets={isLoadingAssets}
|
||||
hasCreatePermission={hasCreatePermission}
|
||||
hasDeletePermission={hasDeletePermission}
|
||||
deletingAssetId={deletingAssetId}
|
||||
onUpload={(files) => runBatchUpload(section, files)}
|
||||
onDeleteAsset={handleDeleteAsset}
|
||||
disabled={!selectedProjectId}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SectionMain>
|
||||
<ToastContainer />
|
||||
|
||||
@ -10,54 +10,31 @@ import LayoutAuthenticated from '../../layouts/Authenticated';
|
||||
import SectionMain from '../../components/SectionMain';
|
||||
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
|
||||
import { getPageTitle } from '../../config';
|
||||
|
||||
type ProjectItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
phase: string;
|
||||
description?: string | null;
|
||||
};
|
||||
import { useAppDispatch, useAppSelector } from '../../stores/hooks';
|
||||
import { fetch as fetchProjects, create as createProject } from '../../stores/projects/projectsSlice';
|
||||
import type { Project } from '../../types/entities';
|
||||
|
||||
const ProjectsListPage = () => {
|
||||
const router = useRouter();
|
||||
const [projects, setProjects] = useState<ProjectItem[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const projects = useAppSelector((state) => state.projects.projects) as Project[];
|
||||
const isLoading = useAppSelector((state) => state.projects.loading);
|
||||
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [isCloning, setIsCloning] = useState(false);
|
||||
const [isCloneOpen, setIsCloneOpen] = useState(false);
|
||||
const [cloneSourceId, setCloneSourceId] = useState('');
|
||||
|
||||
const loadProjects = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
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(() => {
|
||||
dispatch(fetchProjects({ query: '?limit=100&page=0&sort=desc&field=updatedAt' }));
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
loadProjects();
|
||||
}, []);
|
||||
if (projects.length > 0 && !cloneSourceId) {
|
||||
setCloneSourceId(projects[0].id);
|
||||
}
|
||||
}, [projects, cloneSourceId]);
|
||||
|
||||
const buildNewProjectDraft = () => {
|
||||
const stamp = Date.now();
|
||||
@ -76,21 +53,17 @@ const ProjectsListPage = () => {
|
||||
const handleCreateNewProject = async () => {
|
||||
setIsCreating(true);
|
||||
try {
|
||||
const response = await axios.post('/projects', {
|
||||
data: buildNewProjectDraft(),
|
||||
});
|
||||
const createdId = response?.data?.id;
|
||||
const result = await dispatch(createProject(buildNewProjectDraft())).unwrap();
|
||||
const createdId = result?.id;
|
||||
|
||||
if (!createdId) {
|
||||
throw new Error('Project was created but id is missing in response');
|
||||
}
|
||||
|
||||
await router.push(`/projects/${createdId}`);
|
||||
} catch (error: any) {
|
||||
console.error(
|
||||
'Failed to create project:',
|
||||
error?.message || 'Unknown error',
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
console.error('Failed to create project:', errorMessage);
|
||||
toast('Failed to create project', {
|
||||
type: 'error',
|
||||
position: 'bottom-center',
|
||||
@ -119,11 +92,9 @@ const ProjectsListPage = () => {
|
||||
}
|
||||
|
||||
await router.push(`/projects/${createdId}`);
|
||||
} catch (error: any) {
|
||||
console.error(
|
||||
'Failed to clone project:',
|
||||
error?.message || 'Unknown error',
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
console.error('Failed to clone project:', errorMessage);
|
||||
toast('Failed to clone project', {
|
||||
type: 'error',
|
||||
position: 'bottom-center',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user