From b2f641a3985051f57c58b3d77a8b528d0ea57fd4 Mon Sep 17 00:00:00 2001 From: Dmitri Date: Thu, 19 Mar 2026 20:13:16 +0400 Subject: [PATCH] fixed transitions errors --- backend/package.json | 5 + backend/src/db/api/assets.js | 6 +- backend/src/db/api/base.api.js | 19 +- backend/src/db/api/projects.js | 40 +- backend/src/db/api/ui_elements.js | 2 +- backend/src/db/db.config.js | 6 + ...60319000001-add-foreign-key-constraints.js | 150 +++++ ...00002-remove-redundant-deletion-columns.js | 100 ++++ backend/src/db/models/access_logs.js | 8 +- backend/src/db/models/asset_variants.js | 4 +- backend/src/db/models/assets.js | 26 +- backend/src/db/models/page_elements.js | 4 +- backend/src/db/models/page_links.js | 12 +- .../src/db/models/presigned_url_requests.js | 8 +- backend/src/db/models/project_audio_tracks.js | 4 +- backend/src/db/models/project_memberships.js | 8 +- backend/src/db/models/projects.js | 53 +- backend/src/db/models/publish_events.js | 8 +- backend/src/db/models/pwa_caches.js | 4 +- backend/src/db/models/roles.js | 10 +- backend/src/db/models/tour_pages.js | 16 +- backend/src/db/models/transitions.js | 8 +- backend/src/db/models/users.js | 30 +- backend/src/db/sync.js | 7 +- backend/src/factories/router.factory.js | 9 +- backend/src/helpers.js | 4 + backend/src/routes/projects.js | 11 +- backend/src/services/access_logs.js | 138 +---- backend/src/services/asset_variants.js | 138 +---- backend/src/services/assets.js | 138 +---- backend/src/services/file.js | 3 + backend/src/services/page_elements.js | 138 +---- backend/src/services/page_links.js | 138 +---- backend/src/services/permissions.js | 138 +---- .../src/services/presigned_url_requests.js | 138 +---- backend/src/services/project_audio_tracks.js | 3 +- backend/src/services/project_memberships.js | 138 +---- backend/src/services/projects.js | 4 - backend/src/services/publish_events.js | 138 +---- backend/src/services/pwa_caches.js | 138 +---- backend/src/services/roles.js | 3 +- backend/src/services/tour_pages.js | 138 +---- backend/src/services/transitions.js | 138 +---- .../Access_logs/configureAccess_logsCols.tsx | 15 +- .../configureAsset_variantsCols.tsx | 9 +- .../components/Assets/AssetSectionCard.tsx | 154 +++++ .../src/components/Assets/ProjectSelector.tsx | 114 ++++ .../components/Assets/UploadProgressList.tsx | 56 ++ .../components/Assets/configureAssetsCols.tsx | 12 +- .../src/components/Assets/useAssetUploader.ts | 244 ++++++++ .../src/components/Generic/GenericTable.tsx | 533 ----------------- .../configurePage_elementsCols.tsx | 5 +- .../Page_links/configurePage_linksCols.tsx | 11 +- .../Permissions/configurePermissionsCols.tsx | 2 +- .../configurePresigned_url_requestsCols.tsx | 11 +- .../configureProject_audio_tracksCols.tsx | 5 +- .../configureProject_membershipsCols.tsx | 14 +- .../Projects/configureProjectsCols.tsx | 5 +- .../configurePublish_eventsCols.tsx | 14 +- .../Pwa_caches/configurePwa_cachesCols.tsx | 8 +- .../components/Roles/configureRolesCols.tsx | 2 +- .../Tour_pages/configureTour_pagesCols.tsx | 5 +- .../Transitions/configureTransitionsCols.tsx | 5 +- .../src/components/Uploaders/UploadService.js | 37 +- .../components/Users/configureUsersCols.tsx | 7 +- frontend/src/pages/assets/assets-list.tsx | 542 ++---------------- frontend/src/pages/projects/projects-list.tsx | 75 +-- 67 files changed, 1219 insertions(+), 2897 deletions(-) create mode 100644 backend/src/db/migrations/20260319000001-add-foreign-key-constraints.js create mode 100644 backend/src/db/migrations/20260319000002-remove-redundant-deletion-columns.js create mode 100644 frontend/src/components/Assets/AssetSectionCard.tsx create mode 100644 frontend/src/components/Assets/ProjectSelector.tsx create mode 100644 frontend/src/components/Assets/UploadProgressList.tsx create mode 100644 frontend/src/components/Assets/useAssetUploader.ts diff --git a/backend/package.json b/backend/package.json index 72c4241..d9cb074 100644 --- a/backend/package.json +++ b/backend/package.json @@ -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": { diff --git a/backend/src/db/api/assets.js b/backend/src/db/api/assets.js index 117da81..0d966f1 100644 --- a/backend/src/db/api/assets.js +++ b/backend/src/db/api/assets.js @@ -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, }; } diff --git a/backend/src/db/api/base.api.js b/backend/src/db/api/base.api.js index 303dd83..a317bdc 100644 --- a/backend/src/db/api/base.api.js +++ b/backend/src/db/api/base.api.js @@ -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) { diff --git a/backend/src/db/api/projects.js b/backend/src/db/api/projects.js index 655c9f9..d7d65d4 100644 --- a/backend/src/db/api/projects.js +++ b/backend/src/db/api/projects.js @@ -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; diff --git a/backend/src/db/api/ui_elements.js b/backend/src/db/api/ui_elements.js index 26a0b3f..46f2d70 100644 --- a/backend/src/db/api/ui_elements.js +++ b/backend/src/db/api/ui_elements.js @@ -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, })), diff --git a/backend/src/db/db.config.js b/backend/src/db/db.config.js index 2471d98..82d2d3d 100644 --- a/backend/src/db/db.config.js +++ b/backend/src/db/db.config.js @@ -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', } }; diff --git a/backend/src/db/migrations/20260319000001-add-foreign-key-constraints.js b/backend/src/db/migrations/20260319000001-add-foreign-key-constraints.js new file mode 100644 index 0000000..415eb19 --- /dev/null +++ b/backend/src/db/migrations/20260319000001-add-foreign-key-constraints.js @@ -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; + } + } +}; diff --git a/backend/src/db/migrations/20260319000002-remove-redundant-deletion-columns.js b/backend/src/db/migrations/20260319000002-remove-redundant-deletion-columns.js new file mode 100644 index 0000000..0152ba5 --- /dev/null +++ b/backend/src/db/migrations/20260319000002-remove-redundant-deletion-columns.js @@ -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; + } + } +}; diff --git a/backend/src/db/models/access_logs.js b/backend/src/db/models/access_logs.js index 4f18f1e..706ffc1 100644 --- a/backend/src/db/models/access_logs.js +++ b/backend/src/db/models/access_logs.js @@ -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', }); diff --git a/backend/src/db/models/asset_variants.js b/backend/src/db/models/asset_variants.js index d593589..daeb513 100644 --- a/backend/src/db/models/asset_variants.js +++ b/backend/src/db/models/asset_variants.js @@ -114,7 +114,9 @@ size_mb: { foreignKey: { name: 'assetId', }, - constraints: false, + constraints: true, + onDelete: 'CASCADE', + onUpdate: 'CASCADE', }); diff --git a/backend/src/db/models/assets.js b/backend/src/db/models/assets.js index 5617e88..deab765 100644 --- a/backend/src/db/models/assets.js +++ b/backend/src/db/models/assets.js @@ -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', }); diff --git a/backend/src/db/models/page_elements.js b/backend/src/db/models/page_elements.js index ea47c60..592e33c 100644 --- a/backend/src/db/models/page_elements.js +++ b/backend/src/db/models/page_elements.js @@ -163,7 +163,9 @@ content_json: { name: 'pageId', allowNull: false, }, - constraints: false, + constraints: true, + onDelete: 'CASCADE', + onUpdate: 'CASCADE', }); diff --git a/backend/src/db/models/page_links.js b/backend/src/db/models/page_links.js index 7c95eb3..4fab051 100644 --- a/backend/src/db/models/page_links.js +++ b/backend/src/db/models/page_links.js @@ -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', }); diff --git a/backend/src/db/models/presigned_url_requests.js b/backend/src/db/models/presigned_url_requests.js index 06d0aa5..5d73be4 100644 --- a/backend/src/db/models/presigned_url_requests.js +++ b/backend/src/db/models/presigned_url_requests.js @@ -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', }); diff --git a/backend/src/db/models/project_audio_tracks.js b/backend/src/db/models/project_audio_tracks.js index ec1348f..7fb2a5c 100644 --- a/backend/src/db/models/project_audio_tracks.js +++ b/backend/src/db/models/project_audio_tracks.js @@ -135,7 +135,9 @@ is_enabled: { foreignKey: { name: 'projectId', }, - constraints: false, + constraints: true, + onDelete: 'CASCADE', + onUpdate: 'CASCADE', }); diff --git a/backend/src/db/models/project_memberships.js b/backend/src/db/models/project_memberships.js index 6b0d9a3..67ef4ca 100644 --- a/backend/src/db/models/project_memberships.js +++ b/backend/src/db/models/project_memberships.js @@ -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', }); diff --git a/backend/src/db/models/projects.js b/backend/src/db/models/projects.js index 9de1e4e..19ec91f 100644 --- a/backend/src/db/models/projects.js +++ b/backend/src/db/models/projects.js @@ -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', }); diff --git a/backend/src/db/models/publish_events.js b/backend/src/db/models/publish_events.js index 2bff2b5..c600459 100644 --- a/backend/src/db/models/publish_events.js +++ b/backend/src/db/models/publish_events.js @@ -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', }); diff --git a/backend/src/db/models/pwa_caches.js b/backend/src/db/models/pwa_caches.js index 570f0aa..b31d65c 100644 --- a/backend/src/db/models/pwa_caches.js +++ b/backend/src/db/models/pwa_caches.js @@ -104,7 +104,9 @@ is_active: { foreignKey: { name: 'projectId', }, - constraints: false, + constraints: true, + onDelete: 'CASCADE', + onUpdate: 'CASCADE', }); diff --git a/backend/src/db/models/roles.js b/backend/src/db/models/roles.js index 7958fb0..327b107 100644 --- a/backend/src/db/models/roles.js +++ b/backend/src/db/models/roles.js @@ -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', }); diff --git a/backend/src/db/models/tour_pages.js b/backend/src/db/models/tour_pages.js index 060ca95..ab066f2 100644 --- a/backend/src/db/models/tour_pages.js +++ b/backend/src/db/models/tour_pages.js @@ -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', }); diff --git a/backend/src/db/models/transitions.js b/backend/src/db/models/transitions.js index af66c4d..eafc17e 100644 --- a/backend/src/db/models/transitions.js +++ b/backend/src/db/models/transitions.js @@ -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', }); diff --git a/backend/src/db/models/users.js b/backend/src/db/models/users.js index f8f8390..6fd8a5f 100644 --- a/backend/src/db/models/users.js +++ b/backend/src/db/models/users.js @@ -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', diff --git a/backend/src/db/sync.js b/backend/src/db/sync.js index c31db9a..2f921da 100644 --- a/backend/src/db/sync.js +++ b/backend/src/db/sync.js @@ -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) { diff --git a/backend/src/factories/router.factory.js b/backend/src/factories/router.factory.js index 699e399..bc7dc51 100644 --- a/backend/src/factories/router.factory.js +++ b/backend/src/factories/router.factory.js @@ -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)) { diff --git a/backend/src/helpers.js b/backend/src/helpers.js index 1d4d06c..85442de 100644 --- a/backend/src/helpers.js +++ b/backend/src/helpers.js @@ -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); + } }; diff --git a/backend/src/routes/projects.js b/backend/src/routes/projects.js index dc7279d..ce7666c 100644 --- a/backend/src/routes/projects.js +++ b/backend/src/routes/projects.js @@ -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); diff --git a/backend/src/services/access_logs.js b/backend/src/services/access_logs.js index 1c90265..d963bfb 100644 --- a/backend/src/services/access_logs.js +++ b/backend/src/services/access_logs.js @@ -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', +}); diff --git a/backend/src/services/asset_variants.js b/backend/src/services/asset_variants.js index 864828a..e3749e0 100644 --- a/backend/src/services/asset_variants.js +++ b/backend/src/services/asset_variants.js @@ -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', +}); diff --git a/backend/src/services/assets.js b/backend/src/services/assets.js index 65b074d..14c3d9e 100644 --- a/backend/src/services/assets.js +++ b/backend/src/services/assets.js @@ -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', +}); diff --git a/backend/src/services/file.js b/backend/src/services/file.js index c782114..791130c 100644 --- a/backend/src/services/file.js +++ b/backend/src/services/file.js @@ -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); diff --git a/backend/src/services/page_elements.js b/backend/src/services/page_elements.js index d301099..a28b445 100644 --- a/backend/src/services/page_elements.js +++ b/backend/src/services/page_elements.js @@ -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', +}); diff --git a/backend/src/services/page_links.js b/backend/src/services/page_links.js index dc4f61c..549836a 100644 --- a/backend/src/services/page_links.js +++ b/backend/src/services/page_links.js @@ -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', +}); diff --git a/backend/src/services/permissions.js b/backend/src/services/permissions.js index 8ef6399..5e2f070 100644 --- a/backend/src/services/permissions.js +++ b/backend/src/services/permissions.js @@ -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', +}); diff --git a/backend/src/services/presigned_url_requests.js b/backend/src/services/presigned_url_requests.js index 6d08f0a..23b8cb8 100644 --- a/backend/src/services/presigned_url_requests.js +++ b/backend/src/services/presigned_url_requests.js @@ -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', +}); diff --git a/backend/src/services/project_audio_tracks.js b/backend/src/services/project_audio_tracks.js index f9e6aad..f179766 100644 --- a/backend/src/services/project_audio_tracks.js +++ b/backend/src/services/project_audio_tracks.js @@ -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; diff --git a/backend/src/services/project_memberships.js b/backend/src/services/project_memberships.js index d374270..17ae3d7 100644 --- a/backend/src/services/project_memberships.js +++ b/backend/src/services/project_memberships.js @@ -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', +}); diff --git a/backend/src/services/projects.js b/backend/src/services/projects.js index ffcd977..eeb983a 100644 --- a/backend/src/services/projects.js +++ b/backend/src/services/projects.js @@ -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, diff --git a/backend/src/services/publish_events.js b/backend/src/services/publish_events.js index da58954..7175d50 100644 --- a/backend/src/services/publish_events.js +++ b/backend/src/services/publish_events.js @@ -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', +}); diff --git a/backend/src/services/pwa_caches.js b/backend/src/services/pwa_caches.js index d9de732..940af4e 100644 --- a/backend/src/services/pwa_caches.js +++ b/backend/src/services/pwa_caches.js @@ -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', +}); diff --git a/backend/src/services/roles.js b/backend/src/services/roles.js index 790325d..25555ce 100644 --- a/backend/src/services/roles.js +++ b/backend/src/services/roles.js @@ -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; diff --git a/backend/src/services/tour_pages.js b/backend/src/services/tour_pages.js index ec91516..f35914e 100644 --- a/backend/src/services/tour_pages.js +++ b/backend/src/services/tour_pages.js @@ -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', +}); diff --git a/backend/src/services/transitions.js b/backend/src/services/transitions.js index 30d1601..3d4e0e2 100644 --- a/backend/src/services/transitions.js +++ b/backend/src/services/transitions.js @@ -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', +}); diff --git a/frontend/src/components/Access_logs/configureAccess_logsCols.tsx b/frontend/src/components/Access_logs/configureAccess_logsCols.tsx index 190b93a..1d760ea 100644 --- a/frontend/src/components/Access_logs/configureAccess_logsCols.tsx +++ b/frontend/src/components/Access_logs/configureAccess_logsCols.tsx @@ -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), }, { diff --git a/frontend/src/components/Asset_variants/configureAsset_variantsCols.tsx b/frontend/src/components/Asset_variants/configureAsset_variantsCols.tsx index 5c9672b..865b5e7 100644 --- a/frontend/src/components/Asset_variants/configureAsset_variantsCols.tsx +++ b/frontend/src/components/Asset_variants/configureAsset_variantsCols.tsx @@ -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, }, { diff --git a/frontend/src/components/Assets/AssetSectionCard.tsx b/frontend/src/components/Assets/AssetSectionCard.tsx new file mode 100644 index 0000000..bec0254 --- /dev/null +++ b/frontend/src/components/Assets/AssetSectionCard.tsx @@ -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; + 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 = ({ + section, + assets, + uploadQueue, + isUploading, + isLoadingAssets, + hasCreatePermission, + hasDeletePermission, + deletingAssetId, + onUpload, + onDeleteAsset, + disabled, +}) => { + const [isExpanded, setIsExpanded] = useState(false); + + const handleFileChange = (event: React.ChangeEvent) => { + const files = Array.from(event.target.files || []); + onUpload(files); + event.currentTarget.value = ''; + }; + + return ( + +
+

{section.label}

+ {!hasCreatePermission && ( +

+ You do not have upload permission +

+ )} +
+ + + + {isUploading && ( +

+ Uploading {section.label.toLowerCase()} in chunks... +

+ )} + + + + {isLoadingAssets && ( +

Loading assets...

+ )} + + {!isLoadingAssets && assets.length > 0 && ( + + )} + + {!isLoadingAssets && assets.length === 0 && ( +

+ No uploaded {section.label.toLowerCase()}. +

+ )} + + {!isLoadingAssets && assets.length > 0 && isExpanded && ( + + )} +
+ ); +}; + +export default AssetSectionCard; diff --git a/frontend/src/components/Assets/ProjectSelector.tsx b/frontend/src/components/Assets/ProjectSelector.tsx new file mode 100644 index 0000000..19e7c64 --- /dev/null +++ b/frontend/src/components/Assets/ProjectSelector.tsx @@ -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([]); + 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; diff --git a/frontend/src/components/Assets/UploadProgressList.tsx b/frontend/src/components/Assets/UploadProgressList.tsx new file mode 100644 index 0000000..b19f956 --- /dev/null +++ b/frontend/src/components/Assets/UploadProgressList.tsx @@ -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 = ({ items }) => { + const pendingItems = items.filter((item) => item.status !== 'success'); + + if (pendingItems.length === 0) { + return null; + } + + return ( +
    + {pendingItems.map((item) => ( +
  • +
    + {item.fileName} + + {item.status === 'error' + ? 'Error' + : item.status === 'success' + ? 'Done' + : item.status} + +
    + {item.status !== 'success' && ( +
    +
    +
    + )} + {item.error && ( +

    {item.error}

    + )} +
  • + ))} +
+ ); +}; + +export default UploadProgressList; diff --git a/frontend/src/components/Assets/configureAssetsCols.tsx b/frontend/src/components/Assets/configureAssetsCols.tsx index aff139d..a268bf2 100644 --- a/frontend/src/components/Assets/configureAssetsCols.tsx +++ b/frontend/src/components/Assets/configureAssetsCols.tsx @@ -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), }, { diff --git a/frontend/src/components/Assets/useAssetUploader.ts b/frontend/src/components/Assets/useAssetUploader.ts new file mode 100644 index 0000000..d93475e --- /dev/null +++ b/frontend/src/components/Assets/useAssetUploader.ts @@ -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; + runBatchUpload: (section: AssetSection, files: File[]) => Promise; +} + +export function useAssetUploader({ + selectedProjectId, + onUploadComplete, +}: UseAssetUploaderOptions): UseAssetUploaderReturn { + const abortControllerRef = useRef(null); + const [uploadingSections, setUploadingSections] = useState([]); + const [uploadQueues, setUploadQueues] = useState< + Record + >({}); + + // 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) => { + 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; diff --git a/frontend/src/components/Generic/GenericTable.tsx b/frontend/src/components/Generic/GenericTable.tsx index 8efac08..e69de29 100644 --- a/frontend/src/components/Generic/GenericTable.tsx +++ b/frontend/src/components/Generic/GenericTable.tsx @@ -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 { - loading: boolean; - count: number; - refetch: boolean; - notify: NotificationState; - [entityName: string]: T[] | boolean | number | NotificationState | unknown; -} - -// Props for GenericTable -interface GenericTableProps { - entityName: string; - sliceSelector: (state: RootState) => EntitySliceState; - fetchAction: AsyncThunk; - updateAction?: AsyncThunk< - unknown, - { id: string; data: Partial }, - { rejectValue: unknown } - >; - deleteAction: AsyncThunk; - deleteByIdsAction?: AsyncThunk; - setRefetchAction: (value: boolean) => { type: string; payload: boolean }; - loadColumnsFunction: ( - handleDelete: (id: string) => void, - entityPath: string, - currentUser: unknown, - ) => Promise; - filters: Filter[]; - filterItems: FilterItem[]; - setFilterItems: (items: FilterItem[]) => void; - perPage?: number; - showGrid?: boolean; -} - -const GenericTable = ({ - entityName, - sliceSelector, - fetchAction, - updateAction, - deleteAction, - deleteByIdsAction, - setRefetchAction, - loadColumnsFunction, - filters, - filterItems, - setFilterItems, - perPage = 10, -}: GenericTableProps) => { - 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([]); - const [filterRequest, setFilterRequest] = useState(''); - const [selectedRows, setSelectedRows] = useState([]); - const [sortModel, setSortModel] = useState([ - { field: '', sort: 'desc' }, - ]); - - // Delete modal state - const [id, setId] = useState(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) => { - 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, - ) => { - if (!_.isEmpty(rowData) && updateAction) { - await dispatch(updateAction({ id: rowId, data: rowData as Partial })) - .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 = ( -
- '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); - }} - /> -
- ); - - return ( - <> - {filterItems && Array.isArray(filterItems) && filterItems.length ? ( - - null} - > -
- <> - {filterItems.map((filterItem) => { - const selectedFilter = filters.find( - (filter) => - filter.title === filterItem?.fields?.selectedField, - ); - - return ( -
-
-
Filter
- - {filters.map((selectOption) => ( - - ))} - -
- {selectedFilter?.type === 'enum' ? ( -
-
Value
- - - {selectedFilter?.options?.map((option) => ( - - ))} - -
- ) : selectedFilter?.number ? ( -
-
-
From
- -
-
-
To
- -
-
- ) : selectedFilter?.date ? ( -
-
-
From
- -
-
-
To
- -
-
- ) : ( -
-
- Contains -
- -
- )} -
-
Action
- { - deleteFilter(filterItem.id); - }} - /> -
-
- ); - })} -
- - -
- -
-
-
- ) : null} - - -

Are you sure you want to delete this item?

-
- - {dataGrid} - - {selectedRows.length > 0 && - typeof document !== 'undefined' && - document.getElementById('delete-rows-button') && - createPortal( - onDeleteRows(selectedRows)} - />, - document.getElementById('delete-rows-button')!, - )} - - - ); -}; - -export default GenericTable; diff --git a/frontend/src/components/Page_elements/configurePage_elementsCols.tsx b/frontend/src/components/Page_elements/configurePage_elementsCols.tsx index 9fc4983..ffe6724 100644 --- a/frontend/src/components/Page_elements/configurePage_elementsCols.tsx +++ b/frontend/src/components/Page_elements/configurePage_elementsCols.tsx @@ -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, }, { diff --git a/frontend/src/components/Page_links/configurePage_linksCols.tsx b/frontend/src/components/Page_links/configurePage_linksCols.tsx index 6a6644f..0997487 100644 --- a/frontend/src/components/Page_links/configurePage_linksCols.tsx +++ b/frontend/src/components/Page_links/configurePage_linksCols.tsx @@ -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, }, { diff --git a/frontend/src/components/Permissions/configurePermissionsCols.tsx b/frontend/src/components/Permissions/configurePermissionsCols.tsx index 2d46f88..9586b1b 100644 --- a/frontend/src/components/Permissions/configurePermissionsCols.tsx +++ b/frontend/src/components/Permissions/configurePermissionsCols.tsx @@ -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'; diff --git a/frontend/src/components/Presigned_url_requests/configurePresigned_url_requestsCols.tsx b/frontend/src/components/Presigned_url_requests/configurePresigned_url_requestsCols.tsx index 196cbf5..add74d9 100644 --- a/frontend/src/components/Presigned_url_requests/configurePresigned_url_requestsCols.tsx +++ b/frontend/src/components/Presigned_url_requests/configurePresigned_url_requestsCols.tsx @@ -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), }, { diff --git a/frontend/src/components/Project_audio_tracks/configureProject_audio_tracksCols.tsx b/frontend/src/components/Project_audio_tracks/configureProject_audio_tracksCols.tsx index e8d7d55..f9cdc33 100644 --- a/frontend/src/components/Project_audio_tracks/configureProject_audio_tracksCols.tsx +++ b/frontend/src/components/Project_audio_tracks/configureProject_audio_tracksCols.tsx @@ -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, }, { diff --git a/frontend/src/components/Project_memberships/configureProject_membershipsCols.tsx b/frontend/src/components/Project_memberships/configureProject_membershipsCols.tsx index f852730..6769abd 100644 --- a/frontend/src/components/Project_memberships/configureProject_membershipsCols.tsx +++ b/frontend/src/components/Project_memberships/configureProject_membershipsCols.tsx @@ -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), }, { diff --git a/frontend/src/components/Projects/configureProjectsCols.tsx b/frontend/src/components/Projects/configureProjectsCols.tsx index b7d5cc0..30969a7 100644 --- a/frontend/src/components/Projects/configureProjectsCols.tsx +++ b/frontend/src/components/Projects/configureProjectsCols.tsx @@ -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), }, { diff --git a/frontend/src/components/Publish_events/configurePublish_eventsCols.tsx b/frontend/src/components/Publish_events/configurePublish_eventsCols.tsx index b21f7b7..480625b 100644 --- a/frontend/src/components/Publish_events/configurePublish_eventsCols.tsx +++ b/frontend/src/components/Publish_events/configurePublish_eventsCols.tsx @@ -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), }, { diff --git a/frontend/src/components/Pwa_caches/configurePwa_cachesCols.tsx b/frontend/src/components/Pwa_caches/configurePwa_cachesCols.tsx index e790929..e112bf8 100644 --- a/frontend/src/components/Pwa_caches/configurePwa_cachesCols.tsx +++ b/frontend/src/components/Pwa_caches/configurePwa_cachesCols.tsx @@ -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), }, { diff --git a/frontend/src/components/Roles/configureRolesCols.tsx b/frontend/src/components/Roles/configureRolesCols.tsx index 588ff92..03e5175 100644 --- a/frontend/src/components/Roles/configureRolesCols.tsx +++ b/frontend/src/components/Roles/configureRolesCols.tsx @@ -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'; diff --git a/frontend/src/components/Tour_pages/configureTour_pagesCols.tsx b/frontend/src/components/Tour_pages/configureTour_pagesCols.tsx index 68ccf99..f94f4f1 100644 --- a/frontend/src/components/Tour_pages/configureTour_pagesCols.tsx +++ b/frontend/src/components/Tour_pages/configureTour_pagesCols.tsx @@ -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, }, { diff --git a/frontend/src/components/Transitions/configureTransitionsCols.tsx b/frontend/src/components/Transitions/configureTransitionsCols.tsx index 8e91c26..7bb70b7 100644 --- a/frontend/src/components/Transitions/configureTransitionsCols.tsx +++ b/frontend/src/components/Transitions/configureTransitionsCols.tsx @@ -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, }, { diff --git a/frontend/src/components/Uploaders/UploadService.js b/frontend/src/components/Uploaders/UploadService.js index 12ff725..f0d4da8 100644 --- a/frontend/src/components/Uploaders/UploadService.js +++ b/frontend/src/components/Uploaders/UploadService.js @@ -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 diff --git a/frontend/src/components/Users/configureUsersCols.tsx b/frontend/src/components/Users/configureUsersCols.tsx index cbcf83a..e64f876 100644 --- a/frontend/src/components/Users/configureUsersCols.tsx +++ b/frontend/src/components/Users/configureUsersCols.tsx @@ -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) => ( value?.id, getOptionLabel: (value: any) => value?.label, valueOptions: await callOptionsApi('roles'), - valueGetter: (params: GridValueGetterParams) => - params?.value?.id ?? params?.value, + valueGetter: (value) => value?.id ?? value, }, { diff --git a/frontend/src/pages/assets/assets-list.tsx b/frontend/src/pages/assets/assets-list.tsx index 1690731..13a7c52 100644 --- a/frontend/src/pages/assets/assets-list.tsx +++ b/frontend/src/pages/assets/assets-list.tsx @@ -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; - 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([]); - const [selectedProjectId, setSelectedProjectId] = useState(''); - const [isLoadingProjects, setIsLoadingProjects] = useState(false); - const [assets, setAssets] = useState([]); - const [isLoadingAssets, setIsLoadingAssets] = useState(false); - const [uploadingSections, setUploadingSections] = useState([]); - const [uploadQueues, setUploadQueues] = useState< - Record - >({}); - const [expandedUploadedLists, setExpandedUploadedLists] = useState< - Record - >({}); const [deletingAssetId, setDeletingAssetId] = useState(''); + 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, - ) => { - 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>((acc, section) => { @@ -458,143 +151,26 @@ const AssetsTablesPage = () => {

{isLoadingProjects ? 'Loading project...' - : projects.find((project) => project.id === selectedProjectId) - ?.name || 'No project selected'} + : selectedProjectName || 'No project selected'}

- {ASSET_SECTIONS.map((section) => { - const list = assetsBySection[section.key] || []; - const isUploadedListExpanded = - expandedUploadedLists[section.key] ?? false; - - return ( - -
-

{section.label}

- {!hasCreatePermission && ( -

- You do not have upload permission -

- )} -
- { - const files = Array.from(event.target.files || []); - void runBatchUpload(section, files); - event.currentTarget.value = ''; - }} - /> - {uploadingSections.includes(section.key) && ( -

- Uploading {section.label.toLowerCase()} in chunks... -

- )} - - {(uploadQueues[section.key] || []).filter( - (item) => item.status !== 'success', - ).length > 0 && ( -
    - {(uploadQueues[section.key] || []) - .filter((item) => item.status !== 'success') - .map((item) => ( -
  • -
    - {item.fileName} - - {item.status === 'error' - ? 'Error' - : item.status === 'success' - ? 'Done' - : item.status} - -
    - {item.status !== 'success' && ( -
    -
    -
    - )} - {item.error && ( -

    - {item.error} -

    - )} -
  • - ))} -
- )} - - {isLoadingAssets && ( -

Loading assets...

- )} - - {!isLoadingAssets && list.length > 0 && ( - - )} - - {!isLoadingAssets && list.length === 0 && ( -

- No uploaded {section.label.toLowerCase()}. -

- )} - - {!isLoadingAssets && - list.length > 0 && - isUploadedListExpanded && ( - - )} -
- ); - })} + {ASSET_SECTIONS.map((section) => ( + runBatchUpload(section, files)} + onDeleteAsset={handleDeleteAsset} + disabled={!selectedProjectId} + /> + ))}
diff --git a/frontend/src/pages/projects/projects-list.tsx b/frontend/src/pages/projects/projects-list.tsx index 3bade88..ff119bf 100644 --- a/frontend/src/pages/projects/projects-list.tsx +++ b/frontend/src/pages/projects/projects-list.tsx @@ -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([]); - 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',