From 6352be92c9fb0acfed9253e57cccadd14497d14c Mon Sep 17 00:00:00 2001 From: Dave Allison Date: Mon, 2 Jun 2025 11:49:56 +0100 Subject: [PATCH 1/5] Record and display project status changes --- app/controllers/StatusController.scala | 78 ++++++++ app/models/StatusChange.scala | 110 +++++++++++ conf/evolutions/default/34.sql | 27 +++ conf/evolutions/test/34.sql | 110 ++--------- conf/evolutions/test/35.sql | 97 ++++++++++ conf/routes | 3 + .../CommissionEntryEditComponent.tsx | 15 ++ frontend/app/CommissionsList/helpers.ts | 27 +++ .../ProjectEntryEditComponent.tsx | 27 +++ frontend/app/ProjectEntryList/helpers.ts | 28 +++ frontend/app/StatusChanges/StatusChanges.tsx | 177 ++++++++++++++++++ frontend/app/StatusChanges/helpers.ts | 33 ++++ frontend/app/index.jsx | 2 + frontend/types/types.d.ts | 9 + 14 files changed, 653 insertions(+), 90 deletions(-) create mode 100644 app/controllers/StatusController.scala create mode 100644 app/models/StatusChange.scala create mode 100644 conf/evolutions/default/34.sql create mode 100644 conf/evolutions/test/35.sql create mode 100644 frontend/app/StatusChanges/StatusChanges.tsx create mode 100644 frontend/app/StatusChanges/helpers.ts diff --git a/app/controllers/StatusController.scala b/app/controllers/StatusController.scala new file mode 100644 index 00000000..2f3c81a4 --- /dev/null +++ b/app/controllers/StatusController.scala @@ -0,0 +1,78 @@ +package controllers + +import auth.{BearerTokenAuth, Security} +import models.{EntryStatus, ProjectEntry, StatusChangeDAO, StatusChangeSerializer} +import play.api.cache.SyncCacheApi +import play.api.db.slick.DatabaseConfigProvider +import play.api.libs.json._ +import play.api.mvc.{AbstractController, ControllerComponents} +import play.api.{Configuration, Logger} +import slick.jdbc.PostgresProfile +import slick.jdbc.PostgresProfile.api._ +import java.time.ZonedDateTime +import javax.inject._ +import scala.concurrent.ExecutionContext.Implicits.global +import scala.util.{Failure, Success, Try} +import scala.concurrent.Future + +@Singleton +class StatusController @Inject()(cc:ControllerComponents, override val bearerTokenAuth:BearerTokenAuth, + override implicit val config: Configuration, + dbConfigProvider: DatabaseConfigProvider, cacheImpl:SyncCacheApi) + extends AbstractController(cc) with Security with StatusChangeSerializer { + override val logger = Logger(getClass) + + implicit val cache = cacheImpl + implicit val db = dbConfigProvider.get[PostgresProfile].db + + + def record(projectId: Int) = IsAuthenticated { uid => + request => + logger.info(s"Got a status change for project ${projectId}.") + val timestamp = dateTimeToTimestamp(ZonedDateTime.now()) + StatusChangeDAO.create(projectId, timestamp, request.body.asJson.get("user").toString().replace("\"", ""), request.body.asJson.get("status").toString().replace("\"", ""), request.body.asJson.get("title").toString().replace("\"", "")) + Ok(Json.obj("status"->"ok","detail"->"Status change recorded.")) + } + + def recordForCommission(commissionId: Int) = IsAuthenticated { uid => + request => + logger.info(s"Got a status change for commission ${commissionId}.") + val newStatus = EntryStatus.withName(request.body.asJson.get("status").toString().replace("\"", "")) + val action: DBIO[Seq[(Int, ProjectEntry)]] = ProjectEntry.getProjectsEligibleForStatusChange(newStatus, commissionId) + db.run(action).flatMap { projectTuples => + if (projectTuples.isEmpty) { + logger.info(s"StatusChange: No projects found needing status update to $newStatus for commission $commissionId") + Future.successful(Seq.empty) + } else { + logger.info(s"StatusChange: Found ${projectTuples.length} projects to update to $newStatus for commission $commissionId") + logger.info(s"StatusChange: Project IDs to update: ${projectTuples.map(_._1).mkString(", ")}") + projectTuples.foldLeft(Future.successful(Seq.empty[Try[Int]])) { case (accFuture, (id, project)) => + accFuture.flatMap { acc => + val timestamp = dateTimeToTimestamp(ZonedDateTime.now()) + StatusChangeDAO.create(project.id.get, timestamp, request.body.asJson.get("user").toString().replace("\"", ""), request.body.asJson.get("status").toString().replace("\"", ""), project.projectTitle) + val updateAction = (for { + _ <- DBIO.successful() + updateCount = 1 + verification = 1 + } yield (updateCount, verification)).transactionally + db.run(updateAction).map { + case (count, verification) if 1 == 1 => + acc :+ Success(id) + } + } + } + } + } + Ok(Json.obj("status"->"ok","detail"->"Status change recorded.")) + } + + def records(startAt:Int, limit: Int) = IsAdminAsync {uid=>{request=> + StatusChangeDAO.getRecords(startAt, limit).map({ + case Success(results)=>Ok(Json.obj("status"->"ok","result"->results)) + case Failure(error)=> + logger.error("Could not list status changes: ", error) + InternalServerError(Json.obj("status"->"error","detail"->error.toString)) + }) + }} + +} \ No newline at end of file diff --git a/app/models/StatusChange.scala b/app/models/StatusChange.scala new file mode 100644 index 00000000..dc44670c --- /dev/null +++ b/app/models/StatusChange.scala @@ -0,0 +1,110 @@ +package models + +import akka.stream.scaladsl.Source +import slick.jdbc.PostgresProfile.api._ +import slick.lifted.TableQuery +import play.api.Logger +import java.sql.Timestamp +import org.joda.time.DateTime +import org.joda.time.DateTimeZone.UTC +import play.api.libs.functional.syntax._ +import play.api.libs.json._ +import scala.concurrent.Future +import scala.util.{Failure, Success, Try} +import scala.concurrent.ExecutionContext.Implicits.global + +case class StatusChangeDAO (id: Option[Int], projectId: Int, time:Timestamp, user: String, status: String, title: String){ + val logger = Logger(getClass) + def save(implicit db:slick.jdbc.PostgresProfile#Backend#Database):Future[Try[StatusChangeDAO]] = id match { + case None=> + val insertQuery = TableQuery[StatusChange] returning TableQuery[StatusChange].map(_.id) into ((item,id)=>item.copy(id=Some(id))) + db.run( + (insertQuery+=this).asTry + ).map({ + case Success(insertResult)=>Success(insertResult) + case Failure(error)=> + logger.error(s"Inserting change failed due to: $error") + Failure(error) + }) + case Some(realEntityId)=> + db.run( + TableQuery[StatusChange].filter(_.id===realEntityId).update(this).asTry + ).map({ + case Success(rowsAffected)=>Success(this) + case Failure(error)=> + logger.error(s"Updating change failed due to: $error") + Failure(error) + }) + } +} + +object StatusChangeDAO extends ((Option[Int], Int, Timestamp, String, String, String)=>StatusChangeDAO) { + + def create (projectId: Int, time: Timestamp, user: String, status: String, title: String)(implicit db: slick.jdbc.PostgresProfile#Backend#Database): Future[Try[StatusChangeDAO]] = + db.run( + TableQuery[StatusChange].filter(_.id === 0).result + ).map(_.headOption).flatMap({ + case None => + val newRecord = StatusChangeDAO(None, projectId, time, user, status, title) + newRecord.save + }) + + def entryForId(requestedId: Int)(implicit db:slick.jdbc.PostgresProfile#Backend#Database):Future[Try[StatusChangeDAO]] = { + db.run( + TableQuery[StatusChange].filter(_.id===requestedId).result.asTry + ).map(_.map(_.head)) + } + + def entryForIdNew(requestedId: Int)(implicit db:slick.jdbc.PostgresProfile#Backend#Database):Future[StatusChangeDAO] = + db.run( + TableQuery[StatusChange].filter(_.id===requestedId).result + ).map(_.head) + + def scanAllChanges(implicit db:slick.jdbc.PostgresProfile#Backend#Database) = { + Source.fromPublisher(db.stream(TableQuery[StatusChange].sortBy(_.time.desc).result)) + } + + def getRecords(startAt:Int, limit:Int)(implicit db:slick.jdbc.PostgresProfile#Backend#Database) = + db.run( + TableQuery[StatusChange].sortBy(_.id.desc).drop(startAt).take(limit).result.asTry + ) +} + +class StatusChange(tag:Tag) extends Table[StatusChangeDAO](tag, "StatusChange") { + + implicit val DateTimeTotimestamp = + MappedColumnType.base[DateTime, Timestamp]({d=>new Timestamp(d.getMillis)}, {t=>new DateTime(t.getTime, UTC)}) + + def id=column[Int]("id",O.PrimaryKey,O.AutoInc) + def projectId=column[Int]("k_project_id") + def time=column[Timestamp]("t_time") + def user=column[String]("s_user") + def status = column[String]("s_status") + def title = column[String]("s_title") + + def * = (id.?, projectId, time, user, status, title) <> (StatusChangeDAO.tupled, StatusChangeDAO.unapply) + +} + +trait StatusChangeSerializer extends TimestampSerialization { + + implicit val statusChangeWrites:Writes[StatusChangeDAO] = ( + (JsPath \ "id").writeNullable[Int] and + (JsPath \ "projectId").write[Int] and + (JsPath \ "time").write[Timestamp] and + (JsPath \ "user").write[String] and + (JsPath \ "status").write[String] and + (JsPath \ "title").write[String] + )(unlift(StatusChangeDAO.unapply)) + + implicit val statusChangeReads:Reads[StatusChangeDAO] = ( + (JsPath \ "id").readNullable[Int] and + (JsPath \ "projectId").read[Int] and + (JsPath \ "time").read[Timestamp] and + (JsPath \ "user").read[String] and + (JsPath \ "status").read[String] and + (JsPath \ "title").read[String] + )(StatusChangeDAO.apply _) +} + + diff --git a/conf/evolutions/default/34.sql b/conf/evolutions/default/34.sql new file mode 100644 index 00000000..79c89557 --- /dev/null +++ b/conf/evolutions/default/34.sql @@ -0,0 +1,27 @@ +# -- !Ups +CREATE TABLE "StatusChange" ( + id INTEGER NOT NULL PRIMARY KEY, + K_PROJECT_ID INTEGER NOT NULL, + T_TIME TIMESTAMP WITH TIME ZONE NOT NULL, + S_USER CHARACTER VARYING NOT NULL, + S_STATUS VARCHAR(16) NOT NULL, + S_TITLE CHARACTER VARYING +); + +CREATE SEQUENCE "StatusChange_id_seq" +START WITH 1 +INCREMENT BY 1 +NO MINVALUE +NO MAXVALUE +CACHE 1; + +ALTER SEQUENCE "StatusChange_id_seq" OWNED BY "StatusChange".id; + +ALTER TABLE public."StatusChange_id_seq" OWNER TO projectlocker; + +ALTER TABLE "StatusChange" OWNER TO "projectlocker"; + +ALTER TABLE ONLY "StatusChange" ALTER COLUMN id SET DEFAULT nextval('"StatusChange_id_seq"'::regclass); + +# -- !Downs +DROP TABLE "StatusChange" CASCADE; diff --git a/conf/evolutions/test/34.sql b/conf/evolutions/test/34.sql index 39e1130e..79c89557 100644 --- a/conf/evolutions/test/34.sql +++ b/conf/evolutions/test/34.sql @@ -1,97 +1,27 @@ # -- !Ups -INSERT INTO "StorageEntry" (id, S_ROOT_PATH, S_CLIENT_PATH, S_STORAGE_TYPE, S_USER, S_PASSWORD, S_HOST, I_PORT) VALUES (1, '/tmp', NULL, 'Local', 'me', NULL, NULL, NULL); -INSERT INTO "StorageEntry" (id, S_ROOT_PATH, S_CLIENT_PATH, S_STORAGE_TYPE, S_USER, S_PASSWORD, S_HOST, I_PORT) VALUES (2, '/backups/projectfiles', NULL, 'ftp', 'me', '123456abcde', 'ftp.mysite.com', 21); -INSERT INTO "StorageEntry" (id, S_ROOT_PATH, S_CLIENT_PATH, S_STORAGE_TYPE, S_USER, S_PASSWORD, S_HOST, I_PORT, K_BACKS_UP_TO) VALUES (3, '/backups/projectfiles', NULL, 'ftp', 'me', '123456abcde', 'ftp.othermysite.com', 21, 1); +CREATE TABLE "StatusChange" ( + id INTEGER NOT NULL PRIMARY KEY, + K_PROJECT_ID INTEGER NOT NULL, + T_TIME TIMESTAMP WITH TIME ZONE NOT NULL, + S_USER CHARACTER VARYING NOT NULL, + S_STATUS VARCHAR(16) NOT NULL, + S_TITLE CHARACTER VARYING +); -INSERT INTO "FileEntry" (id, S_FILEPATH, K_STORAGE_ID, S_USER, I_VERSION, T_CTIME, T_MTIME, T_ATIME, B_HAS_CONTENT) VALUES (1, '/path/to/a/video.mxf', 2, 'me', 1, '2017-01-17 16:55:00.123', '2017-01-17 16:55:00.123', '2017-01-17 16:55:00.123', false); -INSERT INTO "FileEntry" (id, S_FILEPATH, K_STORAGE_ID, S_USER, I_VERSION, T_CTIME, T_MTIME, T_ATIME, B_HAS_CONTENT) VALUES (2, '/path/to/a/file.project', 1, 'you', 1, '2016-12-11 12:21:11.021', '2016-12-11 12:21:11.021', '2016-12-11 12:21:11.021', true); -INSERT INTO "FileEntry" (id, S_FILEPATH, K_STORAGE_ID, S_USER, I_VERSION, T_CTIME, T_MTIME, T_ATIME, B_HAS_CONTENT) VALUES (3, 'realfile', 1, 'you', 1, '2016-12-11 12:21:11.021', '2016-12-11 12:21:11.021', '2016-12-11 12:21:11.021', true); -INSERT INTO "FileEntry" (id, S_FILEPATH, K_STORAGE_ID, S_USER, I_VERSION, T_CTIME, T_MTIME, T_ATIME, B_HAS_CONTENT) VALUES (4, 'testprojectfile', 1, 'you', 1, '2016-12-11 12:21:11.021', '2016-12-11 12:21:11.021', '2016-12-11 12:21:11.021', false); -INSERT INTO "FileEntry" (id, S_FILEPATH, K_STORAGE_ID, S_USER, I_VERSION, T_CTIME, T_MTIME, T_ATIME, B_HAS_CONTENT) VALUES (5, '/path/to/thattestproject', 1, 'you', 1, '2016-12-11 12:21:11.021', '2016-12-11 12:21:11.021', '2016-12-11 12:21:11.021', true); -INSERT INTO "FileEntry" (id, S_FILEPATH, K_STORAGE_ID, S_USER, I_VERSION, T_CTIME, T_MTIME, T_ATIME, B_HAS_CONTENT) VALUES (6, 'project_to_delete.prproj', 1, 'you', 1, '2016-12-11 12:21:11.021', '2016-12-11 12:21:11.021', '2016-12-11 12:21:11.021', false); -INSERT INTO "FileEntry" (id, S_FILEPATH, K_STORAGE_ID, S_USER, I_VERSION, T_CTIME, T_MTIME, T_ATIME, B_HAS_CONTENT) VALUES (57, 'anothertestprojectfile', 1, 'you', 1, '2016-12-11 12:21:11.021', '2016-12-11 12:21:11.021', '2016-12-11 12:21:11.021', false); -INSERT INTO "FileEntry" (id, S_FILEPATH, K_STORAGE_ID, S_USER, I_VERSION, T_CTIME, T_MTIME, T_ATIME, B_HAS_CONTENT) VALUES (58, '/path/to/a/test.mxf', 3, 'me', 1, '2017-01-17 16:55:00.123', '2017-01-17 16:55:00.123', '2017-01-17 16:55:00.123', false); +CREATE SEQUENCE "StatusChange_id_seq" +START WITH 1 +INCREMENT BY 1 +NO MINVALUE +NO MAXVALUE +CACHE 1; -INSERT INTO "ProjectType" (id, S_NAME, S_OPENS_WITH, S_TARGET_VERSION, S_FILE_EXTENSION) VALUES (1, 'Premiere 2014 test', 'AdobePremierePro.app', '14.0', '.prproj'); -INSERT INTO "ProjectType" (id, S_NAME, S_OPENS_WITH, S_TARGET_VERSION, S_FILE_EXTENSION) VALUES (2, 'Prelude 2014 test', 'AdobePrelude.app', '14.0', '.plproj'); -INSERT INTO "ProjectType" (id, S_NAME, S_OPENS_WITH, S_TARGET_VERSION, S_FILE_EXTENSION) VALUES (3, 'Cubase test', 'Cubase.app', '6.0', '.cpr'); -INSERT INTO "ProjectType" (id, S_NAME, S_OPENS_WITH, S_TARGET_VERSION, S_FILE_EXTENSION) VALUES (4, 'Aftereffects test', 'AdobeAfterEffects.app', '6.0', '.aep'); +ALTER SEQUENCE "StatusChange_id_seq" OWNED BY "StatusChange".id; +ALTER TABLE public."StatusChange_id_seq" OWNER TO projectlocker; -INSERT INTO "ProjectEntry" (id, K_PROJECT_TYPE, S_TITLE, T_CREATED,S_USER) VALUES (1, 1, 'InitialTestProject', '2016-12-11 12:21:11.021', 'me'); -INSERT INTO "ProjectEntry" (id, K_PROJECT_TYPE, S_TITLE, S_VIDISPINE_ID,T_CREATED,S_USER) VALUES (2, 1, 'AnotherTestProject', 'VX-1234', '2016-12-11 12:21:11.021', 'you'); -INSERT INTO "ProjectEntry" (id, K_PROJECT_TYPE, S_TITLE, S_VIDISPINE_ID,T_CREATED,S_USER) VALUES (3, 1, 'ThatTestProject', 'VX-2345', '2016-12-11 12:21:11.021', 'you'); -INSERT INTO "ProjectEntry" (id, K_PROJECT_TYPE, S_TITLE, S_VIDISPINE_ID,T_CREATED,S_USER) VALUES (4, 1, 'WhoseTestProject', 'VX-2345', '2016-12-11 12:21:11.021', 'you'); -INSERT INTO "ProjectEntry" (id, K_PROJECT_TYPE, S_TITLE, S_VIDISPINE_ID,T_CREATED,S_USER) VALUES (5, 1, 'UpgradeTestProject', 'VX-3456', '2016-12-11 12:21:11.021', 'you'); +ALTER TABLE "StatusChange" OWNER TO "projectlocker"; -INSERT INTO "ProjectFileAssociation" (id, K_PROJECT_ENTRY, K_FILE_ENTRY) VALUES (1, 1,2); -INSERT INTO "ProjectFileAssociation" (id, K_PROJECT_ENTRY, K_FILE_ENTRY) VALUES (2, 3,5); +ALTER TABLE ONLY "StatusChange" ALTER COLUMN id SET DEFAULT nextval('"StatusChange_id_seq"'::regclass); -INSERT INTO "ProjectTemplate" (id, S_NAME, K_PROJECT_TYPE, K_FILE_REF) VALUES (1, 'Premiere test template 1', 1,5); -INSERT INTO "ProjectTemplate" (id, S_NAME, K_PROJECT_TYPE, K_FILE_REF) VALUES (2, 'Another wonderful test template', 2, 2); -INSERT INTO "ProjectTemplate" (id, S_NAME, K_PROJECT_TYPE, K_FILE_REF) VALUES (3, 'Some random test template', 2, 2); - -INSERT INTO "PostrunAction" (id, S_RUNNABLE, S_TITLE, S_OWNER, I_VERSION, T_CTIME) VALUES (1, 'FirstTestScript.py', 'First test postrun', 'system',1, '2018-01-01T12:13:24.000'); -INSERT INTO "PostrunAction" (id, S_RUNNABLE, S_TITLE, S_OWNER, I_VERSION, T_CTIME) VALUES (2, 'SecondTestScript.py', 'Second test postrun', 'system',1, '2018-01-01T14:15:31.000'); -INSERT INTO "PostrunAction" (id, S_RUNNABLE, S_TITLE, S_OWNER, I_VERSION, T_CTIME) VALUES (3, 'thirdTestScript.py', 'Third test postrun', 'system',1, '2018-01-01T14:15:31.000'); -INSERT INTO "PostrunAction" (id, S_RUNNABLE, S_TITLE, S_OWNER, I_VERSION, T_CTIME) VALUES (4, 'fourthTestScript.py', 'Fourth test postrun', 'system',1, '2018-01-01T14:15:31.000'); -INSERT INTO "PostrunAction" (id, S_RUNNABLE, S_TITLE, S_OWNER, I_VERSION, T_CTIME) VALUES (5, 'fifthTestScript.py', 'fifth test postrun', 'system',1, '2018-01-01T14:15:31.000'); -INSERT INTO "PostrunAction" (id, S_RUNNABLE, S_TITLE, S_OWNER, I_VERSION, T_CTIME) VALUES (6, 'sixthTestScript.py', 'Sixth test postrun', 'system',1, '2018-01-01T14:15:31.000'); - -INSERT INTO "PostrunAssociationRow" (id, K_POSTRUN, K_PROJECTTYPE) VALUES (1, 1, 1); -INSERT INTO "PostrunAssociationRow" (id, K_POSTRUN, K_PROJECTTYPE) VALUES (2, 2, 1); -INSERT INTO "PostrunAssociationRow" (id, K_POSTRUN, K_PROJECTTYPE) VALUES (4, 5, 1); -INSERT INTO "PostrunAssociationRow" (id, K_POSTRUN, K_PROJECTTYPE) VALUES (3, 2, 4); - -INSERT INTO "PostrunDependency" (id,K_SOURCE, K_DEPENDSON) VALUES (1, 1, 5); -INSERT INTO "PostrunDependency" (id,K_SOURCE, K_DEPENDSON) VALUES (2, 1, 6); -INSERT INTO "PostrunDependency" (id,K_SOURCE, K_DEPENDSON) VALUES (3, 4, 5); -INSERT INTO "PostrunDependency" (id,K_SOURCE, K_DEPENDSON) VALUES (4, 2, 1); - -INSERT INTO "PlutoWorkingGroup" (id, B_HIDE, S_NAME, S_COMMISSIONER) VALUES (1, false, 'Multimedia Social', 'Paul Boyd'); -INSERT INTO "PlutoWorkingGroup" (id, B_HIDE, S_NAME, S_COMMISSIONER) VALUES (2, true, 'Multimedia Anti-Social', 'Boyd Paul'); - -INSERT INTO "PlutoCommission" (id, I_COLLECTION_ID, S_SITE_ID, T_CREATED, T_UPDATED, S_TITLE, S_STATUS, S_DESCRIPTION, K_WORKING_GROUP) VALUES (1, 1234, 'VX', '2018-01-01T12:13:24.000', '2018-01-01T12:13:24.000', 'My test commission', 'New', 'some very long description goes here', 1); -INSERT INTO "PlutoCommission" (id, I_COLLECTION_ID, S_SITE_ID, T_CREATED, T_UPDATED, S_TITLE, S_STATUS, S_DESCRIPTION, K_WORKING_GROUP) VALUES (2, 2345, 'VX', '2018-01-02T12:13:24.000', '2018-01-02T12:13:24.000', 'My test commission 2', 'In Production', 'some very long description goes here', 1); -INSERT INTO "PlutoCommission" (id, I_COLLECTION_ID, S_SITE_ID, T_CREATED, T_UPDATED, S_TITLE, S_STATUS, S_DESCRIPTION, K_WORKING_GROUP) VALUES (3, 3456, 'VX', '2018-01-03T12:13:24.000', '2018-01-03T12:13:24.000', 'My test commission 3', 'Held', 'some very long description goes here', 1); -INSERT INTO "PlutoCommission" (id, I_COLLECTION_ID, S_SITE_ID, T_CREATED, T_UPDATED, S_TITLE, S_STATUS, S_DESCRIPTION, K_WORKING_GROUP) VALUES (4, 4567, 'VX', '2018-01-04T12:13:24.000', '2018-01-04T12:13:24.000', 'My test commission 4', 'Completed', 'some very long description goes here', 1); - -INSERT INTO DEFAULTS (id, S_NAME, S_VALUE) VALUES (1, 'lunch', 'sandwich'); -INSERT INTO DEFAULTS (id, S_NAME, S_VALUE) VALUES (2, 'breakfast', 'toast'); -INSERT INTO DEFAULTS (id, S_NAME, S_VALUE) VALUES (3, 'dessert', 'nothing'); -INSERT INTO DEFAULTS (id, S_NAME, S_VALUE) VALUES (4, 'project_storage_id', 1); - -INSERT INTO "ProjectMetadata" (id, K_PROJECT_ENTRY, S_KEY, S_VALUE) VALUES (1, 2, 'first_key', 'first value'); -INSERT INTO "ProjectMetadata" (id, K_PROJECT_ENTRY, S_KEY, S_VALUE) VALUES (2, 2, 'second_key', 'second value'); - ------------------------- -SELECT pg_catalog.setval('"ProjectMetadata_id_seq"', 3, true); -SELECT pg_catalog.setval('"Defaults_id_seq"', 4, true); -SELECT pg_catalog.setval('"PlutoCommission_id_seq"', 5, true); -SELECT pg_catalog.setval('"PlutoWorkingGroup_id_seq"', 3, true); -SELECT pg_catalog.setval('"PostrunAction_id_seq"', 7, true); -SELECT pg_catalog.setval('"PostrunAssociationRow_id_seq"', 4, true); -SELECT pg_catalog.setval('"ProjectTemplate_id_seq"', 4, true); -SELECT pg_catalog.setval('"ProjectType_id_seq"', 4, true); -SELECT pg_catalog.setval('"StorageEntry_id_seq"', 3, true); -SELECT pg_catalog.setval('"ProjectFileAssociation_id_seq"', 3, false); -SELECT pg_catalog.setval('"ProjectEntry_id_seq"', 6, false); -SELECT pg_catalog.setval('"FileEntry_id_seq"', 6, true); - -# --!Downs -delete from "ProjectMetadata"; -delete from "PlutoWorkingGroup"; -delete from "PlutoProjectType"; -delete from DEFAULTS; -delete from "PlutoCommission"; -delete from "PlutoWorkingGroup"; -delete from "PostrunDependency"; -delete from "PostrunAssociationRow"; -delete from "PostrunAction"; -delete from "ProjectFileAssociation"; -delete from "FileEntry"; -delete from "ProjectEntry"; -delete from "ProjectTemplate"; -delete from "ProjectType"; -delete from "StorageEntry"; -delete from "play_evolutions"; \ No newline at end of file +# -- !Downs +DROP TABLE "StatusChange" CASCADE; diff --git a/conf/evolutions/test/35.sql b/conf/evolutions/test/35.sql new file mode 100644 index 00000000..39e1130e --- /dev/null +++ b/conf/evolutions/test/35.sql @@ -0,0 +1,97 @@ +# -- !Ups +INSERT INTO "StorageEntry" (id, S_ROOT_PATH, S_CLIENT_PATH, S_STORAGE_TYPE, S_USER, S_PASSWORD, S_HOST, I_PORT) VALUES (1, '/tmp', NULL, 'Local', 'me', NULL, NULL, NULL); +INSERT INTO "StorageEntry" (id, S_ROOT_PATH, S_CLIENT_PATH, S_STORAGE_TYPE, S_USER, S_PASSWORD, S_HOST, I_PORT) VALUES (2, '/backups/projectfiles', NULL, 'ftp', 'me', '123456abcde', 'ftp.mysite.com', 21); +INSERT INTO "StorageEntry" (id, S_ROOT_PATH, S_CLIENT_PATH, S_STORAGE_TYPE, S_USER, S_PASSWORD, S_HOST, I_PORT, K_BACKS_UP_TO) VALUES (3, '/backups/projectfiles', NULL, 'ftp', 'me', '123456abcde', 'ftp.othermysite.com', 21, 1); + +INSERT INTO "FileEntry" (id, S_FILEPATH, K_STORAGE_ID, S_USER, I_VERSION, T_CTIME, T_MTIME, T_ATIME, B_HAS_CONTENT) VALUES (1, '/path/to/a/video.mxf', 2, 'me', 1, '2017-01-17 16:55:00.123', '2017-01-17 16:55:00.123', '2017-01-17 16:55:00.123', false); +INSERT INTO "FileEntry" (id, S_FILEPATH, K_STORAGE_ID, S_USER, I_VERSION, T_CTIME, T_MTIME, T_ATIME, B_HAS_CONTENT) VALUES (2, '/path/to/a/file.project', 1, 'you', 1, '2016-12-11 12:21:11.021', '2016-12-11 12:21:11.021', '2016-12-11 12:21:11.021', true); +INSERT INTO "FileEntry" (id, S_FILEPATH, K_STORAGE_ID, S_USER, I_VERSION, T_CTIME, T_MTIME, T_ATIME, B_HAS_CONTENT) VALUES (3, 'realfile', 1, 'you', 1, '2016-12-11 12:21:11.021', '2016-12-11 12:21:11.021', '2016-12-11 12:21:11.021', true); +INSERT INTO "FileEntry" (id, S_FILEPATH, K_STORAGE_ID, S_USER, I_VERSION, T_CTIME, T_MTIME, T_ATIME, B_HAS_CONTENT) VALUES (4, 'testprojectfile', 1, 'you', 1, '2016-12-11 12:21:11.021', '2016-12-11 12:21:11.021', '2016-12-11 12:21:11.021', false); +INSERT INTO "FileEntry" (id, S_FILEPATH, K_STORAGE_ID, S_USER, I_VERSION, T_CTIME, T_MTIME, T_ATIME, B_HAS_CONTENT) VALUES (5, '/path/to/thattestproject', 1, 'you', 1, '2016-12-11 12:21:11.021', '2016-12-11 12:21:11.021', '2016-12-11 12:21:11.021', true); +INSERT INTO "FileEntry" (id, S_FILEPATH, K_STORAGE_ID, S_USER, I_VERSION, T_CTIME, T_MTIME, T_ATIME, B_HAS_CONTENT) VALUES (6, 'project_to_delete.prproj', 1, 'you', 1, '2016-12-11 12:21:11.021', '2016-12-11 12:21:11.021', '2016-12-11 12:21:11.021', false); +INSERT INTO "FileEntry" (id, S_FILEPATH, K_STORAGE_ID, S_USER, I_VERSION, T_CTIME, T_MTIME, T_ATIME, B_HAS_CONTENT) VALUES (57, 'anothertestprojectfile', 1, 'you', 1, '2016-12-11 12:21:11.021', '2016-12-11 12:21:11.021', '2016-12-11 12:21:11.021', false); +INSERT INTO "FileEntry" (id, S_FILEPATH, K_STORAGE_ID, S_USER, I_VERSION, T_CTIME, T_MTIME, T_ATIME, B_HAS_CONTENT) VALUES (58, '/path/to/a/test.mxf', 3, 'me', 1, '2017-01-17 16:55:00.123', '2017-01-17 16:55:00.123', '2017-01-17 16:55:00.123', false); + +INSERT INTO "ProjectType" (id, S_NAME, S_OPENS_WITH, S_TARGET_VERSION, S_FILE_EXTENSION) VALUES (1, 'Premiere 2014 test', 'AdobePremierePro.app', '14.0', '.prproj'); +INSERT INTO "ProjectType" (id, S_NAME, S_OPENS_WITH, S_TARGET_VERSION, S_FILE_EXTENSION) VALUES (2, 'Prelude 2014 test', 'AdobePrelude.app', '14.0', '.plproj'); +INSERT INTO "ProjectType" (id, S_NAME, S_OPENS_WITH, S_TARGET_VERSION, S_FILE_EXTENSION) VALUES (3, 'Cubase test', 'Cubase.app', '6.0', '.cpr'); +INSERT INTO "ProjectType" (id, S_NAME, S_OPENS_WITH, S_TARGET_VERSION, S_FILE_EXTENSION) VALUES (4, 'Aftereffects test', 'AdobeAfterEffects.app', '6.0', '.aep'); + + +INSERT INTO "ProjectEntry" (id, K_PROJECT_TYPE, S_TITLE, T_CREATED,S_USER) VALUES (1, 1, 'InitialTestProject', '2016-12-11 12:21:11.021', 'me'); +INSERT INTO "ProjectEntry" (id, K_PROJECT_TYPE, S_TITLE, S_VIDISPINE_ID,T_CREATED,S_USER) VALUES (2, 1, 'AnotherTestProject', 'VX-1234', '2016-12-11 12:21:11.021', 'you'); +INSERT INTO "ProjectEntry" (id, K_PROJECT_TYPE, S_TITLE, S_VIDISPINE_ID,T_CREATED,S_USER) VALUES (3, 1, 'ThatTestProject', 'VX-2345', '2016-12-11 12:21:11.021', 'you'); +INSERT INTO "ProjectEntry" (id, K_PROJECT_TYPE, S_TITLE, S_VIDISPINE_ID,T_CREATED,S_USER) VALUES (4, 1, 'WhoseTestProject', 'VX-2345', '2016-12-11 12:21:11.021', 'you'); +INSERT INTO "ProjectEntry" (id, K_PROJECT_TYPE, S_TITLE, S_VIDISPINE_ID,T_CREATED,S_USER) VALUES (5, 1, 'UpgradeTestProject', 'VX-3456', '2016-12-11 12:21:11.021', 'you'); + +INSERT INTO "ProjectFileAssociation" (id, K_PROJECT_ENTRY, K_FILE_ENTRY) VALUES (1, 1,2); +INSERT INTO "ProjectFileAssociation" (id, K_PROJECT_ENTRY, K_FILE_ENTRY) VALUES (2, 3,5); + +INSERT INTO "ProjectTemplate" (id, S_NAME, K_PROJECT_TYPE, K_FILE_REF) VALUES (1, 'Premiere test template 1', 1,5); +INSERT INTO "ProjectTemplate" (id, S_NAME, K_PROJECT_TYPE, K_FILE_REF) VALUES (2, 'Another wonderful test template', 2, 2); +INSERT INTO "ProjectTemplate" (id, S_NAME, K_PROJECT_TYPE, K_FILE_REF) VALUES (3, 'Some random test template', 2, 2); + +INSERT INTO "PostrunAction" (id, S_RUNNABLE, S_TITLE, S_OWNER, I_VERSION, T_CTIME) VALUES (1, 'FirstTestScript.py', 'First test postrun', 'system',1, '2018-01-01T12:13:24.000'); +INSERT INTO "PostrunAction" (id, S_RUNNABLE, S_TITLE, S_OWNER, I_VERSION, T_CTIME) VALUES (2, 'SecondTestScript.py', 'Second test postrun', 'system',1, '2018-01-01T14:15:31.000'); +INSERT INTO "PostrunAction" (id, S_RUNNABLE, S_TITLE, S_OWNER, I_VERSION, T_CTIME) VALUES (3, 'thirdTestScript.py', 'Third test postrun', 'system',1, '2018-01-01T14:15:31.000'); +INSERT INTO "PostrunAction" (id, S_RUNNABLE, S_TITLE, S_OWNER, I_VERSION, T_CTIME) VALUES (4, 'fourthTestScript.py', 'Fourth test postrun', 'system',1, '2018-01-01T14:15:31.000'); +INSERT INTO "PostrunAction" (id, S_RUNNABLE, S_TITLE, S_OWNER, I_VERSION, T_CTIME) VALUES (5, 'fifthTestScript.py', 'fifth test postrun', 'system',1, '2018-01-01T14:15:31.000'); +INSERT INTO "PostrunAction" (id, S_RUNNABLE, S_TITLE, S_OWNER, I_VERSION, T_CTIME) VALUES (6, 'sixthTestScript.py', 'Sixth test postrun', 'system',1, '2018-01-01T14:15:31.000'); + +INSERT INTO "PostrunAssociationRow" (id, K_POSTRUN, K_PROJECTTYPE) VALUES (1, 1, 1); +INSERT INTO "PostrunAssociationRow" (id, K_POSTRUN, K_PROJECTTYPE) VALUES (2, 2, 1); +INSERT INTO "PostrunAssociationRow" (id, K_POSTRUN, K_PROJECTTYPE) VALUES (4, 5, 1); +INSERT INTO "PostrunAssociationRow" (id, K_POSTRUN, K_PROJECTTYPE) VALUES (3, 2, 4); + +INSERT INTO "PostrunDependency" (id,K_SOURCE, K_DEPENDSON) VALUES (1, 1, 5); +INSERT INTO "PostrunDependency" (id,K_SOURCE, K_DEPENDSON) VALUES (2, 1, 6); +INSERT INTO "PostrunDependency" (id,K_SOURCE, K_DEPENDSON) VALUES (3, 4, 5); +INSERT INTO "PostrunDependency" (id,K_SOURCE, K_DEPENDSON) VALUES (4, 2, 1); + +INSERT INTO "PlutoWorkingGroup" (id, B_HIDE, S_NAME, S_COMMISSIONER) VALUES (1, false, 'Multimedia Social', 'Paul Boyd'); +INSERT INTO "PlutoWorkingGroup" (id, B_HIDE, S_NAME, S_COMMISSIONER) VALUES (2, true, 'Multimedia Anti-Social', 'Boyd Paul'); + +INSERT INTO "PlutoCommission" (id, I_COLLECTION_ID, S_SITE_ID, T_CREATED, T_UPDATED, S_TITLE, S_STATUS, S_DESCRIPTION, K_WORKING_GROUP) VALUES (1, 1234, 'VX', '2018-01-01T12:13:24.000', '2018-01-01T12:13:24.000', 'My test commission', 'New', 'some very long description goes here', 1); +INSERT INTO "PlutoCommission" (id, I_COLLECTION_ID, S_SITE_ID, T_CREATED, T_UPDATED, S_TITLE, S_STATUS, S_DESCRIPTION, K_WORKING_GROUP) VALUES (2, 2345, 'VX', '2018-01-02T12:13:24.000', '2018-01-02T12:13:24.000', 'My test commission 2', 'In Production', 'some very long description goes here', 1); +INSERT INTO "PlutoCommission" (id, I_COLLECTION_ID, S_SITE_ID, T_CREATED, T_UPDATED, S_TITLE, S_STATUS, S_DESCRIPTION, K_WORKING_GROUP) VALUES (3, 3456, 'VX', '2018-01-03T12:13:24.000', '2018-01-03T12:13:24.000', 'My test commission 3', 'Held', 'some very long description goes here', 1); +INSERT INTO "PlutoCommission" (id, I_COLLECTION_ID, S_SITE_ID, T_CREATED, T_UPDATED, S_TITLE, S_STATUS, S_DESCRIPTION, K_WORKING_GROUP) VALUES (4, 4567, 'VX', '2018-01-04T12:13:24.000', '2018-01-04T12:13:24.000', 'My test commission 4', 'Completed', 'some very long description goes here', 1); + +INSERT INTO DEFAULTS (id, S_NAME, S_VALUE) VALUES (1, 'lunch', 'sandwich'); +INSERT INTO DEFAULTS (id, S_NAME, S_VALUE) VALUES (2, 'breakfast', 'toast'); +INSERT INTO DEFAULTS (id, S_NAME, S_VALUE) VALUES (3, 'dessert', 'nothing'); +INSERT INTO DEFAULTS (id, S_NAME, S_VALUE) VALUES (4, 'project_storage_id', 1); + +INSERT INTO "ProjectMetadata" (id, K_PROJECT_ENTRY, S_KEY, S_VALUE) VALUES (1, 2, 'first_key', 'first value'); +INSERT INTO "ProjectMetadata" (id, K_PROJECT_ENTRY, S_KEY, S_VALUE) VALUES (2, 2, 'second_key', 'second value'); + +------------------------ +SELECT pg_catalog.setval('"ProjectMetadata_id_seq"', 3, true); +SELECT pg_catalog.setval('"Defaults_id_seq"', 4, true); +SELECT pg_catalog.setval('"PlutoCommission_id_seq"', 5, true); +SELECT pg_catalog.setval('"PlutoWorkingGroup_id_seq"', 3, true); +SELECT pg_catalog.setval('"PostrunAction_id_seq"', 7, true); +SELECT pg_catalog.setval('"PostrunAssociationRow_id_seq"', 4, true); +SELECT pg_catalog.setval('"ProjectTemplate_id_seq"', 4, true); +SELECT pg_catalog.setval('"ProjectType_id_seq"', 4, true); +SELECT pg_catalog.setval('"StorageEntry_id_seq"', 3, true); +SELECT pg_catalog.setval('"ProjectFileAssociation_id_seq"', 3, false); +SELECT pg_catalog.setval('"ProjectEntry_id_seq"', 6, false); +SELECT pg_catalog.setval('"FileEntry_id_seq"', 6, true); + +# --!Downs +delete from "ProjectMetadata"; +delete from "PlutoWorkingGroup"; +delete from "PlutoProjectType"; +delete from DEFAULTS; +delete from "PlutoCommission"; +delete from "PlutoWorkingGroup"; +delete from "PostrunDependency"; +delete from "PostrunAssociationRow"; +delete from "PostrunAction"; +delete from "ProjectFileAssociation"; +delete from "FileEntry"; +delete from "ProjectEntry"; +delete from "ProjectTemplate"; +delete from "ProjectType"; +delete from "StorageEntry"; +delete from "play_evolutions"; \ No newline at end of file diff --git a/conf/routes b/conf/routes index 09434224..9931c882 100644 --- a/conf/routes +++ b/conf/routes @@ -70,6 +70,8 @@ GET /api/project/:id/removeWarning @controllers.MissingFilesController. GET /api/project/:id/fileDownload @controllers.ProjectEntryController.fileDownload(id:Int) PUT /api/project/:id/restore/:version @controllers.ProjectEntryController.restoreBackup(id:Int, version:Int) PUT /api/project/:id/restoreForAssetFolder @controllers.ProjectEntryController.restoreAssetFolderBackup(id:Int) +PUT /api/project/:id/statusChange @controllers.StatusController.record(id: Int) +GET /api/statusChanges @controllers.StatusController.records(startAt:Int ?=0,length:Int ?=100) GET /api/valid-users @controllers.ProjectEntryController.queryUsersForAutocomplete(prefix:String ?= "", limit:Option[Int]) GET /api/known-user @controllers.ProjectEntryController.isUserKnown(uname:String ?= "") @@ -116,6 +118,7 @@ OPTIONS /api/pluto/commission/list @controllers.Application.corsOptions PUT /api/pluto/commission/:id @controllers.PlutoCommissionController.updateByAnyone(id:Int) PUT /api/pluto/commission/:id/status @controllers.PlutoCommissionController.updateStatus(id:Int) PUT /api/pluto/commission/:id/deleteData @controllers.ProjectEntryController.deleteCommissionData(id: Int) +PUT /api/pluto/commission/:id/statusChange @controllers.StatusController.recordForCommission(id: Int) GET /api/assetfolder/lookup @controllers.AssetFolderController.assetFolderForPath(path:String) GET /api/project/:projectId/assetfolder @controllers.AssetFolderController.assetFolderForProject(projectId:Int) diff --git a/frontend/app/CommissionsList/CommissionEntryEditComponent.tsx b/frontend/app/CommissionsList/CommissionEntryEditComponent.tsx index 5b4d3456..25fae774 100644 --- a/frontend/app/CommissionsList/CommissionEntryEditComponent.tsx +++ b/frontend/app/CommissionsList/CommissionEntryEditComponent.tsx @@ -37,6 +37,7 @@ import { loadCommissionData, projectsForCommission, updateCommissionData, + recordStatusChange } from "./helpers"; import ErrorIcon from "@material-ui/icons/Error"; import ProductionOfficeSelector from "../common/ProductionOfficeSelector"; @@ -322,6 +323,7 @@ const CommissionEntryEditComponent: React.FC(true); const [order, setOrder] = useState("desc"); const [orderBy, setOrderBy] = useState("created"); + const [userName, setUserName] = useState(''); useEffect(() => { const fetchCommissionData = async () => { @@ -359,6 +361,18 @@ const CommissionEntryEditComponent: React.FC { try { const loggedIn = await isLoggedIn(); + setUserName(loggedIn.uid); setIsAdmin(loggedIn.isAdmin); } catch { setIsAdmin(false); diff --git a/frontend/app/CommissionsList/helpers.ts b/frontend/app/CommissionsList/helpers.ts index ecae4fcb..ce2be8ba 100644 --- a/frontend/app/CommissionsList/helpers.ts +++ b/frontend/app/CommissionsList/helpers.ts @@ -197,3 +197,30 @@ export const startDelete = async ( throw error; } }; + +export const recordStatusChange = async ( + id: number, + user: string, + status_string: string +): Promise => { + try { + const { status } = await Axios.put>( + `${API_COMMISSION}/${id}/statusChange`, + `{"user":"${user}","status":"${status_string}"}`, + { + headers: { + "Content-Type": "application/json", + }, + } + ); + + if (status !== 200) { + throw new Error( + `Could not record status change for commission ${id}: server said ${status}` + ); + } + } catch (error) { + console.error(error); + throw error; + } +}; \ No newline at end of file diff --git a/frontend/app/ProjectEntryList/ProjectEntryEditComponent.tsx b/frontend/app/ProjectEntryList/ProjectEntryEditComponent.tsx index 0051db8e..2b1be8ee 100644 --- a/frontend/app/ProjectEntryList/ProjectEntryEditComponent.tsx +++ b/frontend/app/ProjectEntryList/ProjectEntryEditComponent.tsx @@ -28,6 +28,7 @@ import { getSimpleProjectTypeData, getMissingFiles, downloadProjectFile, + recordStatusChange, } from "./helpers"; import { SystemNotification, @@ -137,6 +138,7 @@ const ProjectEntryEditComponent: React.FC = ( >([]); const [fileData, setFileData] = useState(EMPTY_FILE); const [premiereProVersion, setPremiereProVersion] = useState(1); + const [userName, setUserName] = useState(''); const getProjectTypeData = async (projectTypeId: number) => { try { @@ -203,6 +205,7 @@ const ProjectEntryEditComponent: React.FC = ( const fetchWhoIsLoggedIn = async () => { try { const loggedIn = await isLoggedIn(); + setUserName(loggedIn.uid); setIsAdmin(loggedIn.isAdmin); } catch { setIsAdmin(false); @@ -277,6 +280,18 @@ const ProjectEntryEditComponent: React.FC = ( try { await updateProject(project as Project); + try { + await recordStatusChange( + project.id, + userName, + project.status, + project.title + ); + + } catch { + console.error('Failed to record status change'); + } + SystemNotification.open( SystemNotifcationKind.Success, `Successfully updated project "${project.title}"` @@ -299,6 +314,18 @@ const ProjectEntryEditComponent: React.FC = ( try { await updateProject(project as Project); + try { + await recordStatusChange( + project.id, + userName, + project.status, + project.title + ); + + } catch { + console.error('Failed to record status change'); + } + SystemNotification.open( SystemNotifcationKind.Success, `Successfully updated project "${project.title}"` diff --git a/frontend/app/ProjectEntryList/helpers.ts b/frontend/app/ProjectEntryList/helpers.ts index d2e02b82..d5008951 100644 --- a/frontend/app/ProjectEntryList/helpers.ts +++ b/frontend/app/ProjectEntryList/helpers.ts @@ -624,3 +624,31 @@ export const downloadProjectFile = async (id: number) => { console.log(err); }); }; + +export const recordStatusChange = async ( + id: number, + user: string, + status_string: string, + title: string +): Promise => { + try { + const { status } = await Axios.put>( + `${API_PROJECTS}/${id}/statusChange`, + `{"user":"${user}","status":"${status_string}","title":"${title}"}`, + { + headers: { + "Content-Type": "application/json", + }, + } + ); + + if (status !== 200) { + throw new Error( + `Could not record status change for project ${id}: server said ${status}` + ); + } + } catch (error) { + console.error(error); + throw error; + } +}; diff --git a/frontend/app/StatusChanges/StatusChanges.tsx b/frontend/app/StatusChanges/StatusChanges.tsx new file mode 100644 index 00000000..bbca5947 --- /dev/null +++ b/frontend/app/StatusChanges/StatusChanges.tsx @@ -0,0 +1,177 @@ +import React, { useEffect, useState } from "react"; +import { RouteComponentProps } from "react-router-dom"; +import { + Table, + TableHead, + TableBody, + TableRow, + TableCell, + TableContainer, + Paper, + TablePagination, + TableSortLabel, +} from "@material-ui/core"; +import { getStatusChangesOnPage } from "./helpers"; +import { sortListByOrder, SortDirection } from "../utils/lists"; +import { isLoggedIn } from "../utils/api"; +import { Helmet } from "react-helmet"; +import { useGuardianStyles } from "~/misc/utils"; +import moment from "moment"; + +const tableHeaderTitles: HeaderTitle[] = [ + { label: "Project", key: "projectId" }, + { label: "Title", key: "title" }, + { label: "User", key: "user" }, + { label: "Time", key: "time" }, + { label: "Status", key: "status" }, +]; + +declare var deploymentRootPath: string; + +const pageSizeOptions = [10, 50, 100]; + +const StatusChanges: React.FC = (props) => { + const classes = useGuardianStyles(); + + const [statusChanges, setStatusChanges] = useState([]); + const [isAdmin, setIsAdmin] = useState(false); + const [page, setPage] = useState(0); + const [pageSize, setRowsPerPage] = useState(pageSizeOptions[0]); + const [order, setOrder] = useState("asc"); + const [orderBy, setOrderBy] = useState("id"); + + useEffect(() => { + const fetchDeletionRecordsOnPage = async () => { + const statusChanges = await getStatusChangesOnPage({ page, pageSize }); + setStatusChanges(statusChanges); + }; + + const fetchWhoIsLoggedIn = async () => { + try { + const loggedIn = await isLoggedIn(); + setIsAdmin(loggedIn.isAdmin); + } catch { + setIsAdmin(false); + } + }; + + fetchWhoIsLoggedIn(); + + fetchDeletionRecordsOnPage(); + }, [page, pageSize]); + + const handleChangePage = ( + _event: React.MouseEvent | null, + newPage: number + ) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = async ( + event: React.ChangeEvent + ) => { + setRowsPerPage(+event.target.value); + setPage(0); + }; + + const sortByColumn = (property: keyof StatusChange) => ( + _event: React.MouseEvent + ) => { + const isAsc = orderBy === property && order === "asc"; + setOrder(isAsc ? "desc" : "asc"); + setOrderBy(property); + }; + + return ( + <> + + Status Changes - Pluto Admin + + {isAdmin ? ( + + + + + + {tableHeaderTitles.map((title, index) => ( + + {title.key ? ( + + {title.label} + {orderBy === title.key && ( + + {order === "desc" + ? "sorted descending" + : "sorted ascending"} + + )} + + ) : ( + title.label + )} + + ))} + + + + {sortListByOrder(statusChanges, orderBy, order).map( + ({ + id, + projectId, + time, + user, + status, + title + }) => ( + + window.open( + `${deploymentRootPath}project/${projectId}`, + "_blank" + ) + } + key={id} + > + {projectId} + {title} + {user} + + + {moment(time).format("DD/MM/YYYY HH:mm")} + + + {status} + + ) + )} + +
+
+ + `${from}-${to}`} + /> +
+ ) : ( +
You do not have access to this page.
+ )} + + ); +}; + +export default StatusChanges; diff --git a/frontend/app/StatusChanges/helpers.ts b/frontend/app/StatusChanges/helpers.ts new file mode 100644 index 00000000..752a0a85 --- /dev/null +++ b/frontend/app/StatusChanges/helpers.ts @@ -0,0 +1,33 @@ +import Axios from "axios"; + +const API = "/api"; +const API_STATUS = `${API}/statusChanges`; + +interface StatusChangesOnPage { + page?: number; + pageSize?: number; +} + +export const getStatusChangesOnPage = async ({ + page = 0, + pageSize = 10, +}): Promise => { + try { + const { + status, + data: { result }, + } = await Axios.get>( + `${API_STATUS}?startAt=${page * pageSize}&length=${pageSize}` + ); + + if (status === 200) { + console.log(result); + return result; + } + + throw new Error(`Could not retrieve status changes. ${status}`); + } catch (error) { + console.error(error); + throw error; + } +}; diff --git a/frontend/app/index.jsx b/frontend/app/index.jsx index 7ef5c16d..4bfad73e 100644 --- a/frontend/app/index.jsx +++ b/frontend/app/index.jsx @@ -69,6 +69,7 @@ import CommissionDeleteDataComponent from "./CommissionsList/CommissionDeleteDat import AssetFolderProjectBackups from "./ProjectEntryList/AssetFolderProjectBackups"; import DeletionRecords from "./DeletionRecords/DeletionRecords.tsx"; import DeletionRecord from "./DeletionRecords/DeletionRecord.tsx"; +import StatusChanges from "./StatusChanges/StatusChanges.tsx"; import HelpPage from "./HelpPage/HelpPage.tsx"; library.add(faSearch); @@ -399,6 +400,7 @@ class App extends React.Component { + Date: Mon, 2 Jun 2025 11:51:54 +0100 Subject: [PATCH 2/5] Changes suggested by Yarn --- .../CommissionEntryEditComponent.tsx | 13 +++++----- frontend/app/CommissionsList/helpers.ts | 24 +++++++++---------- .../ProjectEntryEditComponent.tsx | 24 +++++++++---------- frontend/app/ProjectEntryList/helpers.ts | 24 +++++++++---------- frontend/app/StatusChanges/StatusChanges.tsx | 17 ++++--------- 5 files changed, 46 insertions(+), 56 deletions(-) diff --git a/frontend/app/CommissionsList/CommissionEntryEditComponent.tsx b/frontend/app/CommissionsList/CommissionEntryEditComponent.tsx index 25fae774..5a81e44e 100644 --- a/frontend/app/CommissionsList/CommissionEntryEditComponent.tsx +++ b/frontend/app/CommissionsList/CommissionEntryEditComponent.tsx @@ -37,7 +37,7 @@ import { loadCommissionData, projectsForCommission, updateCommissionData, - recordStatusChange + recordStatusChange, } from "./helpers"; import ErrorIcon from "@material-ui/icons/Error"; import ProductionOfficeSelector from "../common/ProductionOfficeSelector"; @@ -323,7 +323,7 @@ const CommissionEntryEditComponent: React.FC(true); const [order, setOrder] = useState("desc"); const [orderBy, setOrderBy] = useState("created"); - const [userName, setUserName] = useState(''); + const [userName, setUserName] = useState(""); useEffect(() => { const fetchCommissionData = async () => { @@ -364,13 +364,12 @@ const CommissionEntryEditComponent: React.FC => { try { const { status } = await Axios.put>( - `${API_COMMISSION}/${id}/statusChange`, - `{"user":"${user}","status":"${status_string}"}`, - { - headers: { - "Content-Type": "application/json", - }, - } + `${API_COMMISSION}/${id}/statusChange`, + `{"user":"${user}","status":"${status_string}"}`, + { + headers: { + "Content-Type": "application/json", + }, + } ); if (status !== 200) { throw new Error( - `Could not record status change for commission ${id}: server said ${status}` + `Could not record status change for commission ${id}: server said ${status}` ); } } catch (error) { console.error(error); throw error; } -}; \ No newline at end of file +}; diff --git a/frontend/app/ProjectEntryList/ProjectEntryEditComponent.tsx b/frontend/app/ProjectEntryList/ProjectEntryEditComponent.tsx index 2b1be8ee..1f7c3179 100644 --- a/frontend/app/ProjectEntryList/ProjectEntryEditComponent.tsx +++ b/frontend/app/ProjectEntryList/ProjectEntryEditComponent.tsx @@ -138,7 +138,7 @@ const ProjectEntryEditComponent: React.FC = ( >([]); const [fileData, setFileData] = useState(EMPTY_FILE); const [premiereProVersion, setPremiereProVersion] = useState(1); - const [userName, setUserName] = useState(''); + const [userName, setUserName] = useState(""); const getProjectTypeData = async (projectTypeId: number) => { try { @@ -282,14 +282,13 @@ const ProjectEntryEditComponent: React.FC = ( try { await recordStatusChange( - project.id, - userName, - project.status, - project.title + project.id, + userName, + project.status, + project.title ); - } catch { - console.error('Failed to record status change'); + console.error("Failed to record status change"); } SystemNotification.open( @@ -316,14 +315,13 @@ const ProjectEntryEditComponent: React.FC = ( try { await recordStatusChange( - project.id, - userName, - project.status, - project.title + project.id, + userName, + project.status, + project.title ); - } catch { - console.error('Failed to record status change'); + console.error("Failed to record status change"); } SystemNotification.open( diff --git a/frontend/app/ProjectEntryList/helpers.ts b/frontend/app/ProjectEntryList/helpers.ts index d5008951..1eb07ec8 100644 --- a/frontend/app/ProjectEntryList/helpers.ts +++ b/frontend/app/ProjectEntryList/helpers.ts @@ -626,25 +626,25 @@ export const downloadProjectFile = async (id: number) => { }; export const recordStatusChange = async ( - id: number, - user: string, - status_string: string, - title: string + id: number, + user: string, + status_string: string, + title: string ): Promise => { try { const { status } = await Axios.put>( - `${API_PROJECTS}/${id}/statusChange`, - `{"user":"${user}","status":"${status_string}","title":"${title}"}`, - { - headers: { - "Content-Type": "application/json", - }, - } + `${API_PROJECTS}/${id}/statusChange`, + `{"user":"${user}","status":"${status_string}","title":"${title}"}`, + { + headers: { + "Content-Type": "application/json", + }, + } ); if (status !== 200) { throw new Error( - `Could not record status change for project ${id}: server said ${status}` + `Could not record status change for project ${id}: server said ${status}` ); } } catch (error) { diff --git a/frontend/app/StatusChanges/StatusChanges.tsx b/frontend/app/StatusChanges/StatusChanges.tsx index bbca5947..bf9e585d 100644 --- a/frontend/app/StatusChanges/StatusChanges.tsx +++ b/frontend/app/StatusChanges/StatusChanges.tsx @@ -122,21 +122,14 @@ const StatusChanges: React.FC = (props) => { {sortListByOrder(statusChanges, orderBy, order).map( - ({ - id, - projectId, - time, - user, - status, - title - }) => ( + ({ id, projectId, time, user, status, title }) => ( - window.open( - `${deploymentRootPath}project/${projectId}`, - "_blank" - ) + window.open( + `${deploymentRootPath}project/${projectId}`, + "_blank" + ) } key={id} > From be3f4b4ee05317822632933cac8b59b8e2e6614b Mon Sep 17 00:00:00 2001 From: Dave Allison Date: Mon, 2 Jun 2025 12:25:18 +0100 Subject: [PATCH 3/5] Record commission status changes before the update code is run --- frontend/app/CommissionsList/CommissionEntryEditComponent.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/app/CommissionsList/CommissionEntryEditComponent.tsx b/frontend/app/CommissionsList/CommissionEntryEditComponent.tsx index 5a81e44e..c5ab704c 100644 --- a/frontend/app/CommissionsList/CommissionEntryEditComponent.tsx +++ b/frontend/app/CommissionsList/CommissionEntryEditComponent.tsx @@ -360,8 +360,6 @@ const CommissionEntryEditComponent: React.FC { setIsSaving(true); try { - await updateCommissionData(updatedCommission); - try { await recordStatusChange( updatedCommission.id, @@ -372,6 +370,8 @@ const CommissionEntryEditComponent: React.FC Date: Mon, 2 Jun 2025 12:50:52 +0100 Subject: [PATCH 4/5] Changing order --- app/models/StatusChange.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/StatusChange.scala b/app/models/StatusChange.scala index dc44670c..83ba214e 100644 --- a/app/models/StatusChange.scala +++ b/app/models/StatusChange.scala @@ -66,7 +66,7 @@ object StatusChangeDAO extends ((Option[Int], Int, Timestamp, String, String, St def getRecords(startAt:Int, limit:Int)(implicit db:slick.jdbc.PostgresProfile#Backend#Database) = db.run( - TableQuery[StatusChange].sortBy(_.id.desc).drop(startAt).take(limit).result.asTry + TableQuery[StatusChange].sortBy(_.id.asc).drop(startAt).take(limit).result.asTry ) } From 8feaae57c32e184bb948d56e3a0f0cb06414c195 Mon Sep 17 00:00:00 2001 From: Dave Allison Date: Mon, 2 Jun 2025 13:22:11 +0100 Subject: [PATCH 5/5] Change sort direction --- app/models/StatusChange.scala | 2 +- frontend/app/StatusChanges/StatusChanges.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/StatusChange.scala b/app/models/StatusChange.scala index 83ba214e..dc44670c 100644 --- a/app/models/StatusChange.scala +++ b/app/models/StatusChange.scala @@ -66,7 +66,7 @@ object StatusChangeDAO extends ((Option[Int], Int, Timestamp, String, String, St def getRecords(startAt:Int, limit:Int)(implicit db:slick.jdbc.PostgresProfile#Backend#Database) = db.run( - TableQuery[StatusChange].sortBy(_.id.asc).drop(startAt).take(limit).result.asTry + TableQuery[StatusChange].sortBy(_.id.desc).drop(startAt).take(limit).result.asTry ) } diff --git a/frontend/app/StatusChanges/StatusChanges.tsx b/frontend/app/StatusChanges/StatusChanges.tsx index bf9e585d..1456f0c5 100644 --- a/frontend/app/StatusChanges/StatusChanges.tsx +++ b/frontend/app/StatusChanges/StatusChanges.tsx @@ -37,7 +37,7 @@ const StatusChanges: React.FC = (props) => { const [isAdmin, setIsAdmin] = useState(false); const [page, setPage] = useState(0); const [pageSize, setRowsPerPage] = useState(pageSizeOptions[0]); - const [order, setOrder] = useState("asc"); + const [order, setOrder] = useState("desc"); const [orderBy, setOrderBy] = useState("id"); useEffect(() => {