diff --git a/.vscode/settings.json b/.vscode/settings.json index 05ef37d87a..840be5bcc8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -29,7 +29,7 @@ "AWS_S3_ECHIMP_BUCKET": "mint-app-echimp-uploads", "AWS_ECHIMP_CR_FILE_NAME": "FFS_CR_DATA.parquet", "AWS_ECHIMP_TDL_FILE_NAME": "TDL_DATA.parquet", - "AWS_ECHIMP_CACHE_TIME_MINS": 180, + "AWS_ECHIMP_CACHE_TIME_MINS": "180", "MINIO_ADDRESS": "http://localhost:9005", "MINIO_ACCESS_KEY": "minioadmin", "MINIO_SECRET_KEY": "minioadmin" diff --git a/MINT.postman_collection.json b/MINT.postman_collection.json index 015250cece..d0c20b894f 100644 --- a/MINT.postman_collection.json +++ b/MINT.postman_collection.json @@ -2863,7 +2863,7 @@ "body": { "mode": "graphql", "graphql": { - "query": "mutation NewMTOCategory {\ncreateMTOCategory(modelPlanID: \"{{modelPlanID}}\",\nname: \"Awesome Category\"\n# ,parentID: \"{{mtoCategoryID}}\"\n) {\n id\n name\n isUncategorized\n }\n}", + "query": "mutation NewMTOCategory {\ncreateMTOCategory(modelPlanID: \"{{modelPlanID}}\",\nname: \"Awesome Category 0\"\n# ,parentID: \"{{mtoCategoryID}}\"\n) {\n id\n name\n isUncategorized\n # modelPlanID\n position\n }\n}", "variables": "" } }, @@ -2877,7 +2877,138 @@ "response": [] }, { - "name": "UpdateMTOCategory", + "name": "4 MTO Categories", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "let responseData = pm.response.json().data", + "", + "mtoCategory0ID = responseData.createCategory0.id", + "pm.collectionVariables.set(\"mtoCategory0ID\", mtoCategory0ID);", + "", + "mtoCategory1ID = responseData.createCategory1.id", + "pm.collectionVariables.set(\"mtoCategory1ID\", mtoCategory1ID);", + "", + "mtoCategory2ID = responseData.createCategory2.id", + "pm.collectionVariables.set(\"mtoCategory2ID\", mtoCategory2ID);", + "", + "mtoCategory3ID = responseData.createCategory3.id", + "pm.collectionVariables.set(\"mtoCategory3ID\", mtoCategory3ID);" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "graphql", + "graphql": { + "query": "mutation NewMTOCategories {\n createCategory0: createMTOCategory(\n modelPlanID: \"{{modelPlanID}}\",\n name: \"Awesome Category 0\"\n ) {\n id\n name\n isUncategorized\n position\n }\n \n createCategory1: createMTOCategory(\n modelPlanID: \"{{modelPlanID}}\",\n name: \"Awesome Category 1\"\n ) {\n id\n name\n isUncategorized\n position\n }\n \n createCategory2: createMTOCategory(\n modelPlanID: \"{{modelPlanID}}\",\n name: \"Awesome Category 2\"\n ) {\n id\n name\n isUncategorized\n position\n }\n \n createCategory3: createMTOCategory(\n modelPlanID: \"{{modelPlanID}}\",\n name: \"Awesome Category 3\"\n ) {\n id\n name\n isUncategorized\n position\n }\n}\n", + "variables": "" + } + }, + "url": { + "raw": "{{url}}", + "host": [ + "{{url}}" + ] + } + }, + "response": [] + }, + { + "name": "16 MTO SubCategories for 4 Parent Categories", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "let responseData = pm.response.json().data;", + "", + "// Subcategories for Category 0", + "let mtoSubcategory00ID = responseData.createSubcategory00.id;", + "pm.collectionVariables.set(\"mtoSubcategory00ID\", mtoSubcategory00ID);", + "", + "let mtoSubcategory01ID = responseData.createSubcategory01.id;", + "pm.collectionVariables.set(\"mtoSubcategory01ID\", mtoSubcategory01ID);", + "", + "let mtoSubcategory02ID = responseData.createSubcategory02.id;", + "pm.collectionVariables.set(\"mtoSubcategory02ID\", mtoSubcategory02ID);", + "", + "let mtoSubcategory03ID = responseData.createSubcategory03.id;", + "pm.collectionVariables.set(\"mtoSubcategory03ID\", mtoSubcategory03ID);", + "", + "// Subcategories for Category 1", + "let mtoSubcategory10ID = responseData.createSubcategory10.id;", + "pm.collectionVariables.set(\"mtoSubcategory10ID\", mtoSubcategory10ID);", + "", + "let mtoSubcategory11ID = responseData.createSubcategory11.id;", + "pm.collectionVariables.set(\"mtoSubcategory11ID\", mtoSubcategory11ID);", + "", + "let mtoSubcategory12ID = responseData.createSubcategory12.id;", + "pm.collectionVariables.set(\"mtoSubcategory12ID\", mtoSubcategory12ID);", + "", + "let mtoSubcategory13ID = responseData.createSubcategory13.id;", + "pm.collectionVariables.set(\"mtoSubcategory13ID\", mtoSubcategory13ID);", + "", + "// Subcategories for Category 2", + "let mtoSubcategory20ID = responseData.createSubcategory20.id;", + "pm.collectionVariables.set(\"mtoSubcategory20ID\", mtoSubcategory20ID);", + "", + "let mtoSubcategory21ID = responseData.createSubcategory21.id;", + "pm.collectionVariables.set(\"mtoSubcategory21ID\", mtoSubcategory21ID);", + "", + "let mtoSubcategory22ID = responseData.createSubcategory22.id;", + "pm.collectionVariables.set(\"mtoSubcategory22ID\", mtoSubcategory22ID);", + "", + "let mtoSubcategory23ID = responseData.createSubcategory23.id;", + "pm.collectionVariables.set(\"mtoSubcategory23ID\", mtoSubcategory23ID);", + "", + "// Subcategories for Category 3", + "let mtoSubcategory30ID = responseData.createSubcategory30.id;", + "pm.collectionVariables.set(\"mtoSubcategory30ID\", mtoSubcategory30ID);", + "", + "let mtoSubcategory31ID = responseData.createSubcategory31.id;", + "pm.collectionVariables.set(\"mtoSubcategory31ID\", mtoSubcategory31ID);", + "", + "let mtoSubcategory32ID = responseData.createSubcategory32.id;", + "pm.collectionVariables.set(\"mtoSubcategory32ID\", mtoSubcategory32ID);", + "", + "let mtoSubcategory33ID = responseData.createSubcategory33.id;", + "pm.collectionVariables.set(\"mtoSubcategory33ID\", mtoSubcategory33ID);", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "graphql", + "graphql": { + "query": "mutation NewMTOSubcategories {\n # Subcategories for Category 0\n createSubcategory00: createMTOCategory(\n modelPlanID: \"{{modelPlanID}}\",\n name: \"Awesome Subcategory 00\",\n parentID: \"{{mtoCategory0ID}}\"\n ) {\n id\n name\n isUncategorized\n position\n }\n createSubcategory01: createMTOCategory(\n modelPlanID: \"{{modelPlanID}}\",\n name: \"Awesome Subcategory 01\",\n parentID: \"{{mtoCategory0ID}}\"\n ) {\n id\n name\n isUncategorized\n position\n }\n createSubcategory02: createMTOCategory(\n modelPlanID: \"{{modelPlanID}}\",\n name: \"Awesome Subcategory 02\",\n parentID: \"{{mtoCategory0ID}}\"\n ) {\n id\n name\n isUncategorized\n position\n }\n createSubcategory03: createMTOCategory(\n modelPlanID: \"{{modelPlanID}}\",\n name: \"Awesome Subcategory 03\",\n parentID: \"{{mtoCategory0ID}}\"\n ) {\n id\n name\n isUncategorized\n position\n }\n\n # Subcategories for Category 1\n createSubcategory10: createMTOCategory(\n modelPlanID: \"{{modelPlanID}}\",\n name: \"Awesome Subcategory 10\",\n parentID: \"{{mtoCategory1ID}}\"\n ) {\n id\n name\n isUncategorized\n position\n }\n createSubcategory11: createMTOCategory(\n modelPlanID: \"{{modelPlanID}}\",\n name: \"Awesome Subcategory 11\",\n parentID: \"{{mtoCategory1ID}}\"\n ) {\n id\n name\n isUncategorized\n position\n }\n createSubcategory12: createMTOCategory(\n modelPlanID: \"{{modelPlanID}}\",\n name: \"Awesome Subcategory 12\",\n parentID: \"{{mtoCategory1ID}}\"\n ) {\n id\n name\n isUncategorized\n position\n }\n createSubcategory13: createMTOCategory(\n modelPlanID: \"{{modelPlanID}}\",\n name: \"Awesome Subcategory 13\",\n parentID: \"{{mtoCategory1ID}}\"\n ) {\n id\n name\n isUncategorized\n position\n }\n\n # Subcategories for Category 2\n createSubcategory20: createMTOCategory(\n modelPlanID: \"{{modelPlanID}}\",\n name: \"Awesome Subcategory 20\",\n parentID: \"{{mtoCategory2ID}}\"\n ) {\n id\n name\n isUncategorized\n position\n }\n createSubcategory21: createMTOCategory(\n modelPlanID: \"{{modelPlanID}}\",\n name: \"Awesome Subcategory 21\",\n parentID: \"{{mtoCategory2ID}}\"\n ) {\n id\n name\n isUncategorized\n position\n }\n createSubcategory22: createMTOCategory(\n modelPlanID: \"{{modelPlanID}}\",\n name: \"Awesome Subcategory 22\",\n parentID: \"{{mtoCategory2ID}}\"\n ) {\n id\n name\n isUncategorized\n position\n }\n createSubcategory23: createMTOCategory(\n modelPlanID: \"{{modelPlanID}}\",\n name: \"Awesome Subcategory 23\",\n parentID: \"{{mtoCategory2ID}}\"\n ) {\n id\n name\n isUncategorized\n position\n }\n\n # Subcategories for Category 3\n createSubcategory30: createMTOCategory(\n modelPlanID: \"{{modelPlanID}}\",\n name: \"Awesome Subcategory 30\",\n parentID: \"{{mtoCategory3ID}}\"\n ) {\n id\n name\n isUncategorized\n position\n }\n createSubcategory31: createMTOCategory(\n modelPlanID: \"{{modelPlanID}}\",\n name: \"Awesome Subcategory 31\",\n parentID: \"{{mtoCategory3ID}}\"\n ) {\n id\n name\n isUncategorized\n position\n }\n createSubcategory32: createMTOCategory(\n modelPlanID: \"{{modelPlanID}}\",\n name: \"Awesome Subcategory 32\",\n parentID: \"{{mtoCategory3ID}}\"\n ) {\n id\n name\n isUncategorized\n position\n }\n createSubcategory33: createMTOCategory(\n modelPlanID: \"{{modelPlanID}}\",\n name: \"Awesome Subcategory 33\",\n parentID: \"{{mtoCategory3ID}}\"\n ) {\n id\n name\n isUncategorized\n position\n }\n}\n", + "variables": "" + } + }, + "url": { + "raw": "{{url}}", + "host": [ + "{{url}}" + ] + } + }, + "response": [] + }, + { + "name": "RenameMTOCategory", "event": [ { "listen": "test", @@ -2901,7 +3032,40 @@ "body": { "mode": "graphql", "graphql": { - "query": "mutation UpdateMTOCategory {\nupdateMTOCategory(id: \"{{mtoCategoryID}}\",\nname: \"Awesome renamed Category\"\n# ,parentID: \"{{mtoCategoryID}}\"\n) {\n id\n name\n isUncategorized\n }\n}", + "query": "mutation RenameMTOCategory {\nrenameMTOCategory(id: \"{{mtoCategoryID}}\",\nname: \"Awesome renamed Category\"\n# ,parentID: \"{{mtoCategoryID}}\"\n) {\n id\n name\n isUncategorized\n }\n}", + "variables": "" + } + }, + "url": { + "raw": "{{url}}", + "host": [ + "{{url}}" + ] + } + }, + "response": [] + }, + { + "name": "Reorder MTO Category", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "graphql", + "graphql": { + "query": "mutation ReorderMTOCategory {\nreorderMTOCategory(id: \"{{mtoCategoryID}}\",\nnewOrder: 0\n) {\n position\n id\n name\n isUncategorized\n }\n}", "variables": "" } }, @@ -2915,7 +3079,7 @@ "response": [] }, { - "name": "UpdateMTOSubcategory", + "name": "RenameMTOSubcategory", "event": [ { "listen": "test", @@ -2939,7 +3103,7 @@ "body": { "mode": "graphql", "graphql": { - "query": "mutation UpdateMTOSubCategory {\nupdateMTOCategory(id: \"{{mtoSubcategoryID}}\",\nname: \"Awesome renamed Sub Category\"\n# ,parentID: \"{{mtoCategoryID}}\"\n) {\n id\n name\n isUncategorized\n }\n}", + "query": "mutation RenameMTOSubcategory {\nrenameMTOCategory(id: \"{{mtoSubcategoryID}}\",\nname: \"Awesome renamed Sub Category\"\n# ,parentID: \"{{mtoCategoryID}}\"\n) {\n id\n name\n isUncategorized\n }\n}", "variables": "" } }, @@ -2977,7 +3141,7 @@ "body": { "mode": "graphql", "graphql": { - "query": "mutation NewMTOCategory {\ncreateMTOCategory(modelPlanID: \"{{modelPlanID}}\",\nname: \"Awesome Sub Category\"\n,parentID: \"{{mtoCategoryID}}\"\n) {\n id\n name\n isUncategorized\n }\n}", + "query": "mutation NewMTOCategory {\ncreateMTOCategory(modelPlanID: \"{{modelPlanID}}\",\nname: \"Awesome Sub Category 0\"\n,parentID: \"{{mtoCategoryID}}\"\n) {\n id\n name\n isUncategorized\n position\n }\n}", "variables": "" } }, @@ -3159,7 +3323,7 @@ "body": { "mode": "graphql", "graphql": { - "query": "query ModelPlanMTOCategories {\nmodelPlan(id: \"{{modelPlanID}}\") {\n mtoMatrix{\n status\n recentEdit{\n modifiedBy\n modifiedDts\n modifiedByUserAccount{\n commonName\n }\n }\n milestones {\n name\n createdDts\n createdBy\n }\n categories{\n id\n name\n isUncategorized\n subCategories {\n id\n name\n isUncategorized\n milestones {\n id\n name\n addedFromMilestoneLibrary\n key\n facilitatedBy\n needBy\n status\n riskIndicator\n # isDraftMilestone\n }\n }\n }\n }\n id\n modelName\n }\n}", + "query": "query ModelPlanMTOCategories {\nmodelPlan(id: \"{{modelPlanID}}\") {\n mtoMatrix{\n status\n recentEdit{\n modifiedBy\n modifiedDts\n modifiedByUserAccount{\n commonName\n }\n }\n milestones {\n name\n createdDts\n createdBy\n }\n categories{\n id\n position\n name\n isUncategorized\n subCategories {\n id\n position\n name\n isUncategorized\n milestones {\n id\n name\n addedFromMilestoneLibrary\n key\n facilitatedBy\n needBy\n status\n riskIndicator\n # isDraftMilestone\n }\n }\n }\n }\n id\n modelName\n }\n}", "variables": "" } }, @@ -3426,6 +3590,86 @@ "key": "mtoCommonMilestoneID", "value": "7b15b8bc-42db-493a-a0c1-1d4148945330", "type": "string" + }, + { + "key": "mtoCategory0ID", + "value": "" + }, + { + "key": "mtoCategory1ID", + "value": "" + }, + { + "key": "mtoCategory2ID", + "value": "" + }, + { + "key": "mtoCategory3ID", + "value": "" + }, + { + "key": "mtoSubcategory00ID", + "value": "" + }, + { + "key": "mtoSubcategory01ID", + "value": "" + }, + { + "key": "mtoSubcategory02ID", + "value": "" + }, + { + "key": "mtoSubcategory03ID", + "value": "" + }, + { + "key": "mtoSubcategory10ID", + "value": "" + }, + { + "key": "mtoSubcategory11ID", + "value": "" + }, + { + "key": "mtoSubcategory12ID", + "value": "" + }, + { + "key": "mtoSubcategory13ID", + "value": "" + }, + { + "key": "mtoSubcategory20ID", + "value": "" + }, + { + "key": "mtoSubcategory21ID", + "value": "" + }, + { + "key": "mtoSubcategory22ID", + "value": "" + }, + { + "key": "mtoSubcategory23ID", + "value": "" + }, + { + "key": "mtoSubcategory30ID", + "value": "" + }, + { + "key": "mtoSubcategory31ID", + "value": "" + }, + { + "key": "mtoSubcategory32ID", + "value": "" + }, + { + "key": "mtoSubcategory33ID", + "value": "" } ] } diff --git a/migrations/V181__Add_MTO_Category.sql b/migrations/V181__Add_MTO_Category.sql index 54cec2c637..38266da0b7 100644 --- a/migrations/V181__Add_MTO_Category.sql +++ b/migrations/V181__Add_MTO_Category.sql @@ -3,6 +3,7 @@ CREATE TABLE mto_category ( name ZERO_STRING NOT NULL, parent_id UUID REFERENCES mto_category(id), model_plan_id UUID NOT NULL REFERENCES model_plan(id), + position INT NOT NULL, created_by UUID NOT NULL REFERENCES user_account(id), created_dts TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, diff --git a/migrations/V188__Add_MTO_Category_Reorder_Trigger.sql b/migrations/V188__Add_MTO_Category_Reorder_Trigger.sql new file mode 100644 index 0000000000..7731fd8828 --- /dev/null +++ b/migrations/V188__Add_MTO_Category_Reorder_Trigger.sql @@ -0,0 +1,112 @@ +CREATE OR REPLACE FUNCTION update_position_based_on_parent_and_model_plan() +RETURNS TRIGGER AS $$ + +DECLARE + /* + This determines if we need to add or subtract a value from the position. It should be +1 or -1 + */ + position_adjustment INT; +BEGIN + + -- Avoid recursion by checking pg_trigger_depth() + -- depth of 0 means not created from inside a trigger + IF pg_trigger_depth() > 1 THEN + RETURN NEW; + END IF; + + -- Handle Insert: Adjust positions of other categories if the inserted position conflicts + IF TG_OP = 'INSERT' THEN + -- Move categories with the same parent_id and model_plan_id that have a position >= NEW.position + position_adjustment := 1; -- For insert, we increase positions of conflicting rows + UPDATE mto_category + SET position = position + position_adjustment, + modified_by = NEW.modified_by, + modified_dts = NEW.modified_dts + WHERE ((parent_id IS NULL AND NEW.parent_id IS NULL) OR (parent_id = NEW.parent_id)) + AND ((model_plan_id IS NULL AND NEW.model_plan_id IS NULL) OR (model_plan_id = NEW.model_plan_id)) + AND position >= NEW.position + AND id != NEW.id; -- Exclude the newly inserted row + + -- Handle Update: Reorder positions when a row's position is changed + ELSIF TG_OP = 'UPDATE' THEN + -- Determine if record moved up or down + IF NEW.position > OLD.position THEN + position_adjustment := -1; + ELSE + position_adjustment := 1; + END IF; + + /* Prevent updates to model_plan_id and parent_id as these are fixed for each category */ + IF OLD.parent_id IS DISTINCT FROM NEW.parent_id AND OLD.model_plan_id IS DISTINCT FROM NEW.model_plan_id THEN + RAISE EXCEPTION 'updating model_plan_id or parent_id is not allows. Caught in trigger function update_position_based_on_parent_and_model_plan'; + ELSIF OLD.parent_id IS NOT DISTINCT FROM NEW.parent_id AND OLD.model_plan_id IS NOT DISTINCT FROM NEW.model_plan_id THEN + IF position_adjustment = -1 THEN + /* Row moved down: Shift rows in the range [OLD.position + 1, NEW.position] up by 1 */ + UPDATE mto_category + SET position = position + position_adjustment, + modified_by = NEW.modified_by, + modified_dts = NEW.modified_dts + WHERE ((parent_id IS NULL AND NEW.parent_id IS NULL) OR (parent_id = NEW.parent_id)) + AND ((model_plan_id IS NULL AND NEW.model_plan_id IS NULL) OR (model_plan_id = NEW.model_plan_id)) + AND position > OLD.position AND position <= NEW.position + AND id != NEW.id; -- Exclude the updated row + ELSIF position_adjustment = 1 THEN + /* Row moved up: Shift rows in the range [NEW.position, OLD.position - 1] down by 1 */ + UPDATE mto_category + SET position = position + position_adjustment, + modified_by = NEW.modified_by, + modified_dts = NEW.modified_dts + WHERE ((parent_id IS NULL AND NEW.parent_id IS NULL) OR (parent_id = NEW.parent_id)) + AND ((model_plan_id IS NULL AND NEW.model_plan_id IS NULL) OR (model_plan_id = NEW.model_plan_id)) + AND position < OLD.position AND position >= NEW.position + AND id != NEW.id; -- Exclude the updated row + END IF; + END IF; + + -- Handle Delete: Move positions up for all categories after the deleted category + ELSIF TG_OP = 'DELETE' THEN + UPDATE mto_category + SET position = position - 1, + modified_by = OLD.modified_by, + modified_dts = OLD.modified_dts + WHERE ((parent_id IS NULL AND OLD.parent_id IS NULL) OR (parent_id = OLD.parent_id)) + AND ((model_plan_id IS NULL AND OLD.model_plan_id IS NULL) OR (model_plan_id = OLD.model_plan_id)) + AND position > OLD.position + AND id != OLD.id; -- Exclude the deleted row + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; +COMMENT ON FUNCTION update_position_based_on_parent_and_model_plan() IS +'This function is a trigger handler that manages adjustments to the position of rows in the mto_category table. +It is invoked automatically after INSERT, UPDATE, or DELETE operations on the mto_category table. +The function performs the following actions: + +1. When a new record is inserted (INSERT): + - It checks for existing records in the same parent-child and model-plan context that have a position greater than or equal to the new record’s position. + - The position of those records is incremented by 1 to make space for the new record at its intended position. + +2. When an existing record is updated (UPDATE): + - It compares the old position with the new position to determine whether the record moved up or down. + - Based on this comparison, it adjusts the positions of other records either by shifting them up or down, ensuring that no gaps or overlaps occur in the sequence. + +3. When a record is deleted (DELETE): + - It decreases the position of all records that are positioned below the deleted record’s original position, effectively "closing the gap" left by the deleted record. + +The function ensures that records are ordered sequentially within the same parent-child and model-plan context, maintaining the integrity of the position values across insert, update, and delete actions.'; + +CREATE TRIGGER update_position_trigger +AFTER INSERT OR UPDATE OR DELETE ON mto_category +FOR EACH ROW +EXECUTE FUNCTION update_position_based_on_parent_and_model_plan(); + + +COMMENT ON TRIGGER update_position_trigger ON mto_category IS +'This trigger is responsible for invoking the update_position_based_on_parent_and_model_plan() function after any INSERT, UPDATE, or DELETE operation on the mto_category table. +It ensures that position adjustments are performed automatically whenever a change is made to a record in the mto_category table. +The trigger operates on each row affected by the INSERT, UPDATE, or DELETE operation, invoking the associated function to handle position management according to the type of operation performed. +- INSERT: The trigger causes the function to adjust the position of other records when a new record is inserted. +- UPDATE: The trigger ensures that any change in position will trigger adjustments for other records that might be affected by the position change. +- DELETE: The trigger makes sure the function handles the case where a record is deleted and the remaining records positions need to be adjusted accordingly. +The trigger guarantees that the integrity of the positions in the mto_category table is always maintained, ensuring proper ordering and minimizing potential conflicts between records.'; diff --git a/pkg/graph/generated/generated.go b/pkg/graph/generated/generated.go index e1cc201a96..fec278afd0 100644 --- a/pkg/graph/generated/generated.go +++ b/pkg/graph/generated/generated.go @@ -362,6 +362,7 @@ type ComplexityRoot struct { ID func(childComplexity int) int IsUncategorized func(childComplexity int) int Name func(childComplexity int) int + Position func(childComplexity int) int SubCategories func(childComplexity int) int } @@ -433,6 +434,7 @@ type ComplexityRoot struct { IsUncategorized func(childComplexity int) int Milestones func(childComplexity int) int Name func(childComplexity int) int + Position func(childComplexity int) int } ModelPlan struct { @@ -535,6 +537,8 @@ type ComplexityRoot struct { MarkAllNotificationsAsRead func(childComplexity int) int MarkNotificationAsRead func(childComplexity int, notificationID uuid.UUID) int RemovePlanDocumentSolutionLinks func(childComplexity int, solutionID uuid.UUID, documentIDs []uuid.UUID) int + RenameMTOCategory func(childComplexity int, id uuid.UUID, name string) int + ReorderMTOCategory func(childComplexity int, id uuid.UUID, newOrder int) int ReportAProblem func(childComplexity int, input model.ReportAProblemInput) int SendFeedbackEmail func(childComplexity int, input model.SendFeedbackEmailInput) int ShareModelPlan func(childComplexity int, modelPlanID uuid.UUID, viewFilter *models.ModelViewFilter, usernames []string, optionalMessage *string) int @@ -542,7 +546,6 @@ type ComplexityRoot struct { UnlockLockableSection func(childComplexity int, modelPlanID uuid.UUID, section models.LockableSection) int UpdateCustomOperationalNeedByID func(childComplexity int, id uuid.UUID, customNeedType *string, needed bool) int UpdateExistingModelLinks func(childComplexity int, modelPlanID uuid.UUID, fieldName models.ExisitingModelLinkFieldType, existingModelIDs []int, currentModelPlanIDs []uuid.UUID) int - UpdateMTOCategory func(childComplexity int, id uuid.UUID, name string) int UpdateMTOMilestone func(childComplexity int, id uuid.UUID, changes map[string]interface{}) int UpdateModelPlan func(childComplexity int, id uuid.UUID, changes map[string]interface{}) int UpdateOperationalSolution func(childComplexity int, id uuid.UUID, changes map[string]interface{}) int @@ -2346,7 +2349,8 @@ type MutationResolver interface { UpdateModelPlan(ctx context.Context, id uuid.UUID, changes map[string]interface{}) (*models.ModelPlan, error) ShareModelPlan(ctx context.Context, modelPlanID uuid.UUID, viewFilter *models.ModelViewFilter, usernames []string, optionalMessage *string) (bool, error) CreateMTOCategory(ctx context.Context, modelPlanID uuid.UUID, name string, parentID *uuid.UUID) (*models.MTOCategory, error) - UpdateMTOCategory(ctx context.Context, id uuid.UUID, name string) (*models.MTOCategory, error) + RenameMTOCategory(ctx context.Context, id uuid.UUID, name string) (*models.MTOCategory, error) + ReorderMTOCategory(ctx context.Context, id uuid.UUID, newOrder int) (*models.MTOCategory, error) CreateMTOMilestoneCustom(ctx context.Context, modelPlanID uuid.UUID, name string, mtoCategoryID *uuid.UUID) (*models.MTOMilestone, error) CreateMTOMilestoneCommon(ctx context.Context, modelPlanID uuid.UUID, commonMilestoneKey models.MTOCommonMilestoneKey) (*models.MTOMilestone, error) UpdateMTOMilestone(ctx context.Context, id uuid.UUID, changes map[string]interface{}) (*models.MTOMilestone, error) @@ -3889,6 +3893,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.MTOCategory.Name(childComplexity), true + case "MTOCategory.position": + if e.complexity.MTOCategory.Position == nil { + break + } + + return e.complexity.MTOCategory.Position(childComplexity), true + case "MTOCategory.subCategories": if e.complexity.MTOCategory.SubCategories == nil { break @@ -4281,6 +4292,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.MTOSubcategory.Name(childComplexity), true + case "MTOSubcategory.position": + if e.complexity.MTOSubcategory.Position == nil { + break + } + + return e.complexity.MTOSubcategory.Position(childComplexity), true + case "ModelPlan.abbreviation": if e.complexity.ModelPlan.Abbreviation == nil { break @@ -4999,6 +5017,30 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.RemovePlanDocumentSolutionLinks(childComplexity, args["solutionID"].(uuid.UUID), args["documentIDs"].([]uuid.UUID)), true + case "Mutation.renameMTOCategory": + if e.complexity.Mutation.RenameMTOCategory == nil { + break + } + + args, err := ec.field_Mutation_renameMTOCategory_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.RenameMTOCategory(childComplexity, args["id"].(uuid.UUID), args["name"].(string)), true + + case "Mutation.reorderMTOCategory": + if e.complexity.Mutation.ReorderMTOCategory == nil { + break + } + + args, err := ec.field_Mutation_reorderMTOCategory_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.ReorderMTOCategory(childComplexity, args["id"].(uuid.UUID), args["newOrder"].(int)), true + case "Mutation.reportAProblem": if e.complexity.Mutation.ReportAProblem == nil { break @@ -5083,18 +5125,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.UpdateExistingModelLinks(childComplexity, args["modelPlanID"].(uuid.UUID), args["fieldName"].(models.ExisitingModelLinkFieldType), args["existingModelIDs"].([]int), args["currentModelPlanIDs"].([]uuid.UUID)), true - case "Mutation.updateMTOCategory": - if e.complexity.Mutation.UpdateMTOCategory == nil { - break - } - - args, err := ec.field_Mutation_updateMTOCategory_args(context.TODO(), rawArgs) - if err != nil { - return 0, false - } - - return e.complexity.Mutation.UpdateMTOCategory(childComplexity, args["id"].(uuid.UUID), args["name"].(string)), true - case "Mutation.updateMTOMilestone": if e.complexity.Mutation.UpdateMTOMilestone == nil { break @@ -16563,6 +16593,7 @@ type MTOCategory { # DB Fields id: UUID! # TODO: If we handle "Uncategorized" as a real category, maybe it won't actually have an ID? name: String! + position: Int! # Custom Resolvers @@ -16574,6 +16605,7 @@ type MTOSubcategory { # DB Fields id: UUID! # TODO: If we handle "Uncategorized" as a real category, maybe it won't actually have an ID? name: String! + position: Int! # Custom Resolvers isUncategorized: Boolean! @@ -16591,7 +16623,10 @@ extend type Mutation { Allows you to rename an MTO category. Notably, name is the only field that can be updated. You cannot have a duplicate name per model plan and parent. If the change makes a conflict, this will error. """ - updateMTOCategory(id: UUID!, name: String!): MTOCategory! + renameMTOCategory(id: UUID!, name: String!): MTOCategory! + @hasRole(role: MINT_USER) + + reorderMTOCategory(id: UUID!, newOrder: Int!): MTOCategory! @hasRole(role: MINT_USER) }`, BuiltIn: false}, {Name: "../schema/types/mto_common_milestone.graphql", Input: `enum MTOCommonMilestoneKey { @@ -21298,6 +21333,54 @@ func (ec *executionContext) field_Mutation_removePlanDocumentSolutionLinks_args( return args, nil } +func (ec *executionContext) field_Mutation_renameMTOCategory_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 uuid.UUID + if tmp, ok := rawArgs["id"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) + arg0, err = ec.unmarshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, tmp) + if err != nil { + return nil, err + } + } + args["id"] = arg0 + var arg1 string + if tmp, ok := rawArgs["name"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) + arg1, err = ec.unmarshalNString2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["name"] = arg1 + return args, nil +} + +func (ec *executionContext) field_Mutation_reorderMTOCategory_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 uuid.UUID + if tmp, ok := rawArgs["id"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) + arg0, err = ec.unmarshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, tmp) + if err != nil { + return nil, err + } + } + args["id"] = arg0 + var arg1 int + if tmp, ok := rawArgs["newOrder"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("newOrder")) + arg1, err = ec.unmarshalNInt2int(ctx, tmp) + if err != nil { + return nil, err + } + } + args["newOrder"] = arg1 + return args, nil +} + func (ec *executionContext) field_Mutation_reportAProblem_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -21484,30 +21567,6 @@ func (ec *executionContext) field_Mutation_updateExistingModelLinks_args(ctx con return args, nil } -func (ec *executionContext) field_Mutation_updateMTOCategory_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { - var err error - args := map[string]interface{}{} - var arg0 uuid.UUID - if tmp, ok := rawArgs["id"]; ok { - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) - arg0, err = ec.unmarshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, tmp) - if err != nil { - return nil, err - } - } - args["id"] = arg0 - var arg1 string - if tmp, ok := rawArgs["name"]; ok { - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("name")) - arg1, err = ec.unmarshalNString2string(ctx, tmp) - if err != nil { - return nil, err - } - } - args["name"] = arg1 - return args, nil -} - func (ec *executionContext) field_Mutation_updateMTOMilestone_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -30715,6 +30774,50 @@ func (ec *executionContext) fieldContext_MTOCategory_name(ctx context.Context, f return fc, nil } +func (ec *executionContext) _MTOCategory_position(ctx context.Context, field graphql.CollectedField, obj *models.MTOCategory) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_MTOCategory_position(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Position, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(int) + fc.Result = res + return ec.marshalNInt2int(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_MTOCategory_position(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "MTOCategory", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _MTOCategory_isUncategorized(ctx context.Context, field graphql.CollectedField, obj *models.MTOCategory) (ret graphql.Marshaler) { fc, err := ec.fieldContext_MTOCategory_isUncategorized(ctx, field) if err != nil { @@ -30802,6 +30905,8 @@ func (ec *executionContext) fieldContext_MTOCategory_subCategories(ctx context.C return ec.fieldContext_MTOSubcategory_id(ctx, field) case "name": return ec.fieldContext_MTOSubcategory_name(ctx, field) + case "position": + return ec.fieldContext_MTOSubcategory_position(ctx, field) case "isUncategorized": return ec.fieldContext_MTOSubcategory_isUncategorized(ctx, field) case "milestones": @@ -32396,6 +32501,8 @@ func (ec *executionContext) fieldContext_MTOMilestone_category(ctx context.Conte return ec.fieldContext_MTOCategory_id(ctx, field) case "name": return ec.fieldContext_MTOCategory_name(ctx, field) + case "position": + return ec.fieldContext_MTOCategory_position(ctx, field) case "isUncategorized": return ec.fieldContext_MTOCategory_isUncategorized(ctx, field) case "subCategories": @@ -32450,6 +32557,8 @@ func (ec *executionContext) fieldContext_MTOMilestone_subCategory(ctx context.Co return ec.fieldContext_MTOSubcategory_id(ctx, field) case "name": return ec.fieldContext_MTOSubcategory_name(ctx, field) + case "position": + return ec.fieldContext_MTOSubcategory_position(ctx, field) case "isUncategorized": return ec.fieldContext_MTOSubcategory_isUncategorized(ctx, field) case "milestones": @@ -33332,6 +33441,50 @@ func (ec *executionContext) fieldContext_MTOSubcategory_name(ctx context.Context return fc, nil } +func (ec *executionContext) _MTOSubcategory_position(ctx context.Context, field graphql.CollectedField, obj *models.MTOSubcategory) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_MTOSubcategory_position(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Position, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(int) + fc.Result = res + return ec.marshalNInt2int(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_MTOSubcategory_position(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "MTOSubcategory", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _MTOSubcategory_isUncategorized(ctx context.Context, field graphql.CollectedField, obj *models.MTOSubcategory) (ret graphql.Marshaler) { fc, err := ec.fieldContext_MTOSubcategory_isUncategorized(ctx, field) if err != nil { @@ -37373,6 +37526,8 @@ func (ec *executionContext) fieldContext_ModelsToOperationMatrix_categories(ctx return ec.fieldContext_MTOCategory_id(ctx, field) case "name": return ec.fieldContext_MTOCategory_name(ctx, field) + case "position": + return ec.fieldContext_MTOCategory_position(ctx, field) case "isUncategorized": return ec.fieldContext_MTOCategory_isUncategorized(ctx, field) case "subCategories": @@ -38652,6 +38807,8 @@ func (ec *executionContext) fieldContext_Mutation_createMTOCategory(ctx context. return ec.fieldContext_MTOCategory_id(ctx, field) case "name": return ec.fieldContext_MTOCategory_name(ctx, field) + case "position": + return ec.fieldContext_MTOCategory_position(ctx, field) case "isUncategorized": return ec.fieldContext_MTOCategory_isUncategorized(ctx, field) case "subCategories": @@ -38674,8 +38831,99 @@ func (ec *executionContext) fieldContext_Mutation_createMTOCategory(ctx context. return fc, nil } -func (ec *executionContext) _Mutation_updateMTOCategory(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Mutation_updateMTOCategory(ctx, field) +func (ec *executionContext) _Mutation_renameMTOCategory(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_renameMTOCategory(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().RenameMTOCategory(rctx, fc.Args["id"].(uuid.UUID), fc.Args["name"].(string)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + role, err := ec.unmarshalNRole2githubᚗcomᚋcmsᚑenterpriseᚋmintᚑappᚋpkgᚋgraphᚋmodelᚐRole(ctx, "MINT_USER") + if err != nil { + return nil, err + } + if ec.directives.HasRole == nil { + return nil, errors.New("directive hasRole is not implemented") + } + return ec.directives.HasRole(ctx, nil, directive0, role) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*models.MTOCategory); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/cms-enterprise/mint-app/pkg/models.MTOCategory`, tmp) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*models.MTOCategory) + fc.Result = res + return ec.marshalNMTOCategory2ᚖgithubᚗcomᚋcmsᚑenterpriseᚋmintᚑappᚋpkgᚋmodelsᚐMTOCategory(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_renameMTOCategory(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_MTOCategory_id(ctx, field) + case "name": + return ec.fieldContext_MTOCategory_name(ctx, field) + case "position": + return ec.fieldContext_MTOCategory_position(ctx, field) + case "isUncategorized": + return ec.fieldContext_MTOCategory_isUncategorized(ctx, field) + case "subCategories": + return ec.fieldContext_MTOCategory_subCategories(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type MTOCategory", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_renameMTOCategory_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_reorderMTOCategory(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_reorderMTOCategory(ctx, field) if err != nil { return graphql.Null } @@ -38689,7 +38937,7 @@ func (ec *executionContext) _Mutation_updateMTOCategory(ctx context.Context, fie resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { directive0 := func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().UpdateMTOCategory(rctx, fc.Args["id"].(uuid.UUID), fc.Args["name"].(string)) + return ec.resolvers.Mutation().ReorderMTOCategory(rctx, fc.Args["id"].(uuid.UUID), fc.Args["newOrder"].(int)) } directive1 := func(ctx context.Context) (interface{}, error) { role, err := ec.unmarshalNRole2githubᚗcomᚋcmsᚑenterpriseᚋmintᚑappᚋpkgᚋgraphᚋmodelᚐRole(ctx, "MINT_USER") @@ -38729,7 +38977,7 @@ func (ec *executionContext) _Mutation_updateMTOCategory(ctx context.Context, fie return ec.marshalNMTOCategory2ᚖgithubᚗcomᚋcmsᚑenterpriseᚋmintᚑappᚋpkgᚋmodelsᚐMTOCategory(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Mutation_updateMTOCategory(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Mutation_reorderMTOCategory(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Mutation", Field: field, @@ -38741,6 +38989,8 @@ func (ec *executionContext) fieldContext_Mutation_updateMTOCategory(ctx context. return ec.fieldContext_MTOCategory_id(ctx, field) case "name": return ec.fieldContext_MTOCategory_name(ctx, field) + case "position": + return ec.fieldContext_MTOCategory_position(ctx, field) case "isUncategorized": return ec.fieldContext_MTOCategory_isUncategorized(ctx, field) case "subCategories": @@ -38756,7 +39006,7 @@ func (ec *executionContext) fieldContext_Mutation_updateMTOCategory(ctx context. } }() ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Mutation_updateMTOCategory_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + if fc.Args, err = ec.field_Mutation_reorderMTOCategory_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { ec.Error(ctx, err) return fc, err } @@ -133500,6 +133750,11 @@ func (ec *executionContext) _MTOCategory(ctx context.Context, sel ast.SelectionS if out.Values[i] == graphql.Null { atomic.AddUint32(&out.Invalids, 1) } + case "position": + out.Values[i] = ec._MTOCategory_position(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } case "isUncategorized": out.Values[i] = ec._MTOCategory_isUncategorized(ctx, field, obj) if out.Values[i] == graphql.Null { @@ -134179,6 +134434,11 @@ func (ec *executionContext) _MTOSubcategory(ctx context.Context, sel ast.Selecti if out.Values[i] == graphql.Null { atomic.AddUint32(&out.Invalids, 1) } + case "position": + out.Values[i] = ec._MTOSubcategory_position(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } case "isUncategorized": out.Values[i] = ec._MTOSubcategory_isUncategorized(ctx, field, obj) if out.Values[i] == graphql.Null { @@ -135846,9 +136106,16 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { out.Invalids++ } - case "updateMTOCategory": + case "renameMTOCategory": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_renameMTOCategory(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "reorderMTOCategory": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { - return ec._Mutation_updateMTOCategory(ctx, field) + return ec._Mutation_reorderMTOCategory(ctx, field) }) if out.Values[i] == graphql.Null { out.Invalids++ diff --git a/pkg/graph/resolvers/mto_category.go b/pkg/graph/resolvers/mto_category.go index 0b705da7e0..a2e78e1205 100644 --- a/pkg/graph/resolvers/mto_category.go +++ b/pkg/graph/resolvers/mto_category.go @@ -23,7 +23,8 @@ func MTOCategoryCreate(ctx context.Context, logger *zap.Logger, principal authen if principalAccount == nil { return nil, fmt.Errorf("principal doesn't have an account, username %s", principal.String()) } - category := models.NewMTOCategory(principalAccount.ID, name, modelPlanID, parentID) + // Note, the position added is not respected when inserted. It will be have a position equal to the max of all other positions + category := models.NewMTOCategory(principalAccount.ID, name, modelPlanID, parentID, 0) err := BaseStructPreCreate(logger, category, principal, store, true) if err != nil { @@ -32,8 +33,8 @@ func MTOCategoryCreate(ctx context.Context, logger *zap.Logger, principal authen return storage.MTOCategoryCreate(store, logger, category) } -// MTOCategoryUpdate updates the name of MTOCategory or SubCategory -func MTOCategoryUpdate(ctx context.Context, logger *zap.Logger, principal authentication.Principal, store *storage.Store, +// MTOCategoryRename updates the name of MTOCategory or SubCategory +func MTOCategoryRename(ctx context.Context, logger *zap.Logger, principal authentication.Principal, store *storage.Store, id uuid.UUID, name string, ) (*models.MTOCategory, error) { @@ -56,31 +57,60 @@ func MTOCategoryUpdate(ctx context.Context, logger *zap.Logger, principal authen return storage.MTOCategoryUpdate(store, logger, existing) } +// MTOCategoryReorder updates the position of an MTOCategory or SubCategory +func MTOCategoryReorder(ctx context.Context, logger *zap.Logger, principal authentication.Principal, store *storage.Store, + id uuid.UUID, + order int, +) (*models.MTOCategory, error) { + principalAccount := principal.Account() + if principalAccount == nil { + return nil, fmt.Errorf("principal doesn't have an account, username %s", principal.String()) + } + existing, err := storage.MTOCategoryGetByID(store, logger, id) + if err != nil { + return nil, fmt.Errorf("unable to update MTO category. Err %w", err) + } + // update the position to the new value + // the re-ordering of other rows is handled in the trigger added in migrations/V188__Add_MTO_Category_Reorder_Trigger.sql + existing.Position = order + + // Just check access, don't apply changes here + err = BaseStructPreUpdate(logger, existing, map[string]interface{}{}, principal, store, false, true) + if err != nil { + return nil, err + } + return storage.MTOCategoryUpdate(store, logger, existing) +} + // MTOCategoryGetByModelPlanIDLOADER implements resolver logic to get all parent level MTO Categories by a model plan ID using a data loader func MTOCategoryGetByModelPlanIDLOADER(ctx context.Context, modelPlanID uuid.UUID) ([]*models.MTOCategory, error) { dbCategories, err := loaders.MTOCategory.ByModelPlanID.Load(ctx, modelPlanID) if err != nil { return nil, err } + // return while adding an uncategorized record as well - return append(dbCategories, models.MTOUncategorized(modelPlanID, nil)), nil + return append(dbCategories, models.MTOUncategorizedFromArray(modelPlanID, nil, dbCategories)), nil } // MTOCategoryAndSubcategoriesGetByModelPlanIDLOADER implements resolver logic to get all MTO Categories (including subcategories) by a model plan ID using a data loader func MTOCategoryAndSubcategoriesGetByModelPlanIDLOADER(ctx context.Context, modelPlanID uuid.UUID) ([]*models.MTOCategory, error) { + //TODO, consider removing this. It probably doesn't make sense conceptually, as you are only making on uncategorized + // , and giving it a max position of all categories + dbCategories, err := loaders.MTOCategory.AndSubCategoriesByModelPlanID.Load(ctx, modelPlanID) if err != nil { return nil, err } // return while adding an uncategorized record as well - return append(dbCategories, models.MTOUncategorized(modelPlanID, nil)), nil + return append(dbCategories, models.MTOUncategorizedFromArray(modelPlanID, nil, dbCategories)), nil } func MTOSubcategoryGetByParentIDLoader(ctx context.Context, modelPlanID uuid.UUID, parentID uuid.UUID) ([]*models.MTOSubcategory, error) { - dbCategories, err := loaders.MTOSubcategory.ByParentID.Load(ctx, parentID) + dbSubcategories, err := loaders.MTOSubcategory.ByParentID.Load(ctx, parentID) if err != nil { return nil, err } // return while adding an uncategorized record as well - return append(dbCategories, models.MTOUncategorizedSubcategory(modelPlanID, &parentID)), nil + return append(dbSubcategories, models.MTOUncategorizedSubcategoryFromArray(modelPlanID, &parentID, dbSubcategories)), nil } diff --git a/pkg/graph/resolvers/mto_category.resolvers.go b/pkg/graph/resolvers/mto_category.resolvers.go index 860de31467..0617c843b9 100644 --- a/pkg/graph/resolvers/mto_category.resolvers.go +++ b/pkg/graph/resolvers/mto_category.resolvers.go @@ -43,12 +43,19 @@ func (r *mutationResolver) CreateMTOCategory(ctx context.Context, modelPlanID uu return MTOCategoryCreate(ctx, logger, principal, r.store, name, modelPlanID, parentID) } -// UpdateMTOCategory is the resolver for the updateMTOCategory field. -func (r *mutationResolver) UpdateMTOCategory(ctx context.Context, id uuid.UUID, name string) (*models.MTOCategory, error) { +// RenameMTOCategory is the resolver for the renameMTOCategory field. +func (r *mutationResolver) RenameMTOCategory(ctx context.Context, id uuid.UUID, name string) (*models.MTOCategory, error) { principal := appcontext.Principal(ctx) logger := appcontext.ZLogger(ctx) - return MTOCategoryUpdate(ctx, logger, principal, r.store, id, name) + return MTOCategoryRename(ctx, logger, principal, r.store, id, name) +} + +// ReorderMTOCategory is the resolver for the reorderMTOCategory field. +func (r *mutationResolver) ReorderMTOCategory(ctx context.Context, id uuid.UUID, newOrder int) (*models.MTOCategory, error) { + principal := appcontext.Principal(ctx) + logger := appcontext.ZLogger(ctx) + return MTOCategoryReorder(ctx, logger, principal, r.store, id, newOrder) } // MTOCategory returns generated.MTOCategoryResolver implementation. diff --git a/pkg/graph/resolvers/mto_category_test.go b/pkg/graph/resolvers/mto_category_test.go index 7ed34ef8fa..142dfd5518 100644 --- a/pkg/graph/resolvers/mto_category_test.go +++ b/pkg/graph/resolvers/mto_category_test.go @@ -1,5 +1,426 @@ package resolvers +import ( + "github.com/google/uuid" + + "github.com/cms-enterprise/mint-app/pkg/models" +) + +func (suite *ResolverSuite) createMTOCategory(catName string, modelPlanID uuid.UUID, parentID *uuid.UUID) *models.MTOCategory { + category, err := MTOCategoryCreate(suite.testConfigs.Context, suite.testConfigs.Logger, suite.testConfigs.Principal, suite.testConfigs.Store, catName, modelPlanID, parentID) + suite.NoError(err) + suite.NoError(err) + + suite.Equal(catName, category.Name) + suite.Equal(suite.testConfigs.Principal.UserAccount.ID, category.CreatedBy) + suite.NotNil(category.CreatedDts) + if parentID == nil { + suite.Nil(category.ParentID, "parent ID wasn't provided, so the category should have a nil category id") + } else { + if suite.NotNil(category.ParentID) { + suite.EqualValues(*parentID, *category.ParentID, "the returned category doesn't have a parent id as expected") + } + + } + + suite.Nil(category.ModifiedBy) + suite.Nil(category.ModifiedDts) + suite.EqualValues(modelPlanID, category.ModelPlanID) + + return category +} + +// createSubcategories creates multiple subcategories under a specified parent category. +// Each subcategory is added with a unique position under the parent category. +func (suite *ResolverSuite) createMultipleMTOcategories(categoryNames []string, modelPlanID uuid.UUID, parentID *uuid.UUID) []*models.MTOCategory { + var subcategories []*models.MTOCategory + + for _, name := range categoryNames { + subcategory := suite.createMTOCategory(name, modelPlanID, parentID) + subcategories = append(subcategories, subcategory) + } + + return subcategories +} + func (suite *ResolverSuite) TestMTOCategoryGetByModelPlanIDLOADER() { //TODO when data exchange approach is complete, use the generic testing functionality introduced to write a unit test for this loader } + +// TestMTOCategoryCreate validates fields are generated and categories are created as expected for an MTO category +// Special emphasis on the order of the category when it is placed into a model +func (suite *ResolverSuite) TestMTOCategoryCreate() { + + plan := suite.createModelPlan("testing category creation plan") + // Make top level and sub categories + cat1Name := "Category 1" + cat1SubAName := "Category 1A" + cat1SubBName := "Category 1B" + + category1, err := MTOCategoryCreate(suite.testConfigs.Context, suite.testConfigs.Logger, suite.testConfigs.Principal, suite.testConfigs.Store, cat1Name, plan.ID, nil) + suite.NoError(err) + // Assert all fields are as expected for first model + suite.Equal(0, category1.Position, "Categories should be added to the next available position") + suite.Equal(cat1Name, category1.Name) + suite.Equal(suite.testConfigs.Principal.UserAccount.ID, category1.CreatedBy) + suite.NotNil(category1.CreatedDts) + suite.Nil(category1.ParentID) + suite.Nil(category1.ModifiedBy) + suite.Nil(category1.ModifiedDts) + suite.EqualValues(plan.ID, category1.ModelPlanID) + + category1SubA, err := MTOCategoryCreate(suite.testConfigs.Context, suite.testConfigs.Logger, suite.testConfigs.Principal, suite.testConfigs.Store, cat1SubAName, plan.ID, &category1.ID) + suite.NoError(err) + suite.Equal(0, category1SubA.Position, "Categories should be added to the next available position") + if suite.NotNil(category1SubA.ParentID) { + suite.EqualValues(category1.ID, *category1SubA.ParentID) + } + suite.EqualValues(plan.ID, category1SubA.ModelPlanID) + + category1SubB, err := MTOCategoryCreate(suite.testConfigs.Context, suite.testConfigs.Logger, suite.testConfigs.Principal, suite.testConfigs.Store, cat1SubBName, plan.ID, &category1.ID) + suite.NoError(err) + suite.Equal(1, category1SubB.Position, "Categories should be added to the next available position") + if suite.NotNil(category1SubB.ParentID) { + suite.EqualValues(category1.ID, *category1SubB.ParentID) + } + suite.EqualValues(plan.ID, category1SubB.ModelPlanID) + + cat2Name := "Category 2" + cat2SubAName := "Category 2A" + cat2SubBName := "Category 2B" + + category2, err := MTOCategoryCreate(suite.testConfigs.Context, suite.testConfigs.Logger, suite.testConfigs.Principal, suite.testConfigs.Store, cat2Name, plan.ID, nil) + suite.NoError(err) + //Second top level category + suite.Equal(1, category2.Position, "Categories should be added to the next available position") + + category2SubA, err := MTOCategoryCreate(suite.testConfigs.Context, suite.testConfigs.Logger, suite.testConfigs.Principal, suite.testConfigs.Store, cat2SubAName, plan.ID, &category2.ID) + suite.NoError(err) + suite.Equal(0, category2SubA.Position, "Categories should be added to the next available position") + category2SubB, err := MTOCategoryCreate(suite.testConfigs.Context, suite.testConfigs.Logger, suite.testConfigs.Principal, suite.testConfigs.Store, cat2SubBName, plan.ID, &category2.ID) + suite.NoError(err) + suite.Equal(1, category2SubB.Position, "Categories should be added to the next available position") + + cat3Name := "Category 3" + + category3, err := MTOCategoryCreate(suite.testConfigs.Context, suite.testConfigs.Logger, suite.testConfigs.Principal, suite.testConfigs.Store, cat3Name, plan.ID, nil) + suite.NoError(err) + //Third top level category + suite.Equal(2, category3.Position, "Categories should be added to the next available position") + +} + +func (suite *ResolverSuite) TestMTOCategoryRename() { + plan := suite.createModelPlan("testing category creation plan") + // Make top level and sub categories + cat1Name := "Category 1" + cat1Rename := "Category 1 Renamed Hooray!" + + category1, err := MTOCategoryCreate(suite.testConfigs.Context, suite.testConfigs.Logger, suite.testConfigs.Principal, suite.testConfigs.Store, cat1Name, plan.ID, nil) + suite.NoError(err) + // Assert all fields are as expected for first model + suite.Equal(0, category1.Position, "Categories should be added to the next available position") + suite.Equal(cat1Name, category1.Name) + suite.Equal(suite.testConfigs.Principal.UserAccount.ID, category1.CreatedBy) + suite.NotNil(category1.CreatedDts) + suite.Nil(category1.ParentID) + suite.Nil(category1.ModifiedBy) + suite.Nil(category1.ModifiedDts) + suite.EqualValues(plan.ID, category1.ModelPlanID) + + renamedCategory, err := MTOCategoryRename(suite.testConfigs.Context, suite.testConfigs.Logger, suite.testConfigs.Principal, suite.testConfigs.Store, + category1.ID, cat1Rename) + suite.NoError(err) + suite.EqualValues(cat1Rename, renamedCategory.Name) + // This didn't affect the position + suite.EqualValues(0, renamedCategory.Position) + // This didn't affect model plan id + suite.EqualValues(plan.ID, renamedCategory.ModelPlanID) + +} + +func (suite *ResolverSuite) TestMTOCategoryCreationAndDefaults() { + plan := suite.createModelPlan("Testing Plan for Default Categories") + + // Test creation of top-level category + topCategory := suite.createMTOCategory("Top Category", plan.ID, nil) + suite.Equal(0, topCategory.Position, "Top-level categories should start at position 0") + + // Test creation of subcategory with default position + subCategory := suite.createMTOCategory("Sub Category A", plan.ID, &topCategory.ID) + suite.Equal(0, subCategory.Position, "Subcategories should start at position 0 within their parent") +} + +func (suite *ResolverSuite) TestMultipleMTOCategoriesWithPositioning() { + plan := suite.createModelPlan("Testing Plan for Multiple Categories") + cat0Name := "Category 0" + cat1Name := "Category 1" + cat2Name := "Category 2" + + names := []string{cat0Name, cat1Name, cat2Name} + categories := suite.createMultipleMTOcategories(names, plan.ID, nil) + suite.Len(categories, 3) + + // Create multiple categories and assert position are correct + cat0, cat1, cat2 := categories[0], categories[1], categories[2] + suite.EqualValues(cat0Name, cat0.Name) + suite.EqualValues(cat1Name, cat1.Name) + suite.EqualValues(cat2Name, cat2.Name) + + suite.Equal(0, cat0.Position) + suite.Equal(1, cat1.Position, "Category 1 should be positioned at 1 after Category 0") + suite.Equal(2, cat2.Position, "Category 2 should be positioned at 2 after Category 1") + + plan2 := suite.createModelPlan("Testing Plan for Multiple Categories") + plan2Cat := suite.createMTOCategory("placeholder", plan2.ID, nil) + suite.Equal(0, plan2Cat.Position) + +} + +// TestMTOCategoryReorderToPositionZero validates that category updates happen as expected when moving to a higher position +func (suite *ResolverSuite) TestMTOCategoryReorderToPositionZero() { + plan := suite.createModelPlan("Testing Plan for Reordering") + + // Create multiple categories for model plan + cat0Name := "Category 0" + cat1Name := "Category 1" + cat2Name := "Category 2" + cat2SubName := "Category 2 Sub" + names := []string{cat0Name, cat1Name, cat2Name} + categories := suite.createMultipleMTOcategories(names, plan.ID, nil) + suite.Len(categories, 3) + + cat0, cat1, cat2 := categories[0], categories[1], categories[2] + + // Make child for category 2 + cat2ChildCategory := suite.createMTOCategory(cat2SubName, plan.ID, &cat2.ID) + suite.Equal(0, cat2ChildCategory.Position) + + // Create a category for another model plan + plan2 := suite.createModelPlan("Testing Plan for Multiple Categories") + plan2Cat := suite.createMTOCategory("placeholder", plan2.ID, nil) + plan2CatSub := suite.createMTOCategory("placeholder sub", plan2.ID, &plan2Cat.ID) + suite.Equal(0, plan2Cat.Position) + suite.Equal(0, plan2CatSub.Position) + + // Move cat2 to position 0 and verify reordering + _, err := MTOCategoryReorder(suite.testConfigs.Context, suite.testConfigs.Logger, suite.testConfigs.Principal, suite.testConfigs.Store, cat2.ID, 0) + suite.NoError(err) + + // Verify positions after reordering + retCategories, err := MTOCategoryGetByModelPlanIDLOADER(suite.testConfigs.Context, plan.ID) + suite.NoError(err) + suite.Len(retCategories, 4) + + suite.EqualValues(cat2.ID, retCategories[0].ID, "Category 2 should now be in position 0") + suite.EqualValues(cat0.ID, retCategories[1].ID, "Category 0 should move to position 1") + suite.EqualValues(cat1.ID, retCategories[2].ID, "Category 1 should move to position 2") + suite.EqualValues(uuid.Nil, retCategories[3].ID, "Uncategorized should have the max position (3)") + suite.EqualValues(3, retCategories[3].Position, "Uncategorized should have uuid.Nil") + + //Verify that a child category isn't updated if parent is reordered + retSubCategories, err := MTOSubcategoryGetByParentIDLoader(suite.testConfigs.Context, plan.ID, cat2.ID) + suite.NoError(err) + suite.Len(retSubCategories, 2, "There should be two sub categories (including uncategorized)") + suite.EqualValues(cat2ChildCategory.ID, retSubCategories[0].ID, "Category 2 Sub should remain in position 0") + + /***** verify that a category for another model plan isn't updated + *****/ + // Verify positions after reordering for other Model Plan + retCategoriesPLan2, err := MTOCategoryGetByModelPlanIDLOADER(suite.testConfigs.Context, plan2.ID) + suite.NoError(err) + suite.Len(retCategoriesPLan2, 2) + //Verify that a child category isn't updated if parent is reordered + retSubCategoriesPlan2, err := MTOSubcategoryGetByParentIDLoader(suite.testConfigs.Context, plan2.ID, plan2Cat.ID) + suite.NoError(err) + suite.Len(retSubCategoriesPlan2, 2, "There should be two sub categories (including uncategorized)") + suite.EqualValues(plan2CatSub.ID, retSubCategoriesPlan2[0].ID, "Category 2 Sub should remain in position 0") + /*end */ +} + +// TestMTOCategoryReorderToPositionTwo validates that category updates happen as expected when moving to a lower position +func (suite *ResolverSuite) TestMTOCategoryReorderToPositionTwo() { + plan := suite.createModelPlan("Testing Plan for Reordering") + + // Create multiple categories for model plan + cat0Name := "Category 0" + cat1Name := "Category 1" + cat2Name := "Category 2" + cat2SubName := "Category 2 Sub" + names := []string{cat0Name, cat1Name, cat2Name} + categories := suite.createMultipleMTOcategories(names, plan.ID, nil) + suite.Len(categories, 3) + + cat0, cat1, cat2 := categories[0], categories[1], categories[2] + + // Make child for category 2 + cat2ChildCategory := suite.createMTOCategory(cat2SubName, plan.ID, &cat2.ID) + suite.Equal(0, cat2ChildCategory.Position) + + // Create a category for another model plan + plan2 := suite.createModelPlan("Testing Plan for Multiple Categories") + plan2Cat := suite.createMTOCategory("placeholder", plan2.ID, nil) + plan2CatSub := suite.createMTOCategory("placeholder sub", plan2.ID, &plan2Cat.ID) + suite.Equal(0, plan2Cat.Position) + suite.Equal(0, plan2CatSub.Position) + + // Move cat0 to position 2 and verify reordering + _, err := MTOCategoryReorder(suite.testConfigs.Context, suite.testConfigs.Logger, suite.testConfigs.Principal, suite.testConfigs.Store, cat0.ID, 2) + suite.NoError(err) + + // Verify positions after reordering + retCategories, err := MTOCategoryGetByModelPlanIDLOADER(suite.testConfigs.Context, plan.ID) + suite.NoError(err) + suite.Len(retCategories, 4) + + suite.EqualValues(cat1.ID, retCategories[0].ID, "Category 1 should now be in position 0") + suite.EqualValues(cat2.ID, retCategories[1].ID, "Category 2 should move to position 1") + suite.EqualValues(cat0.ID, retCategories[2].ID, "Category 0 should move to position 2") + + //Verify that a child category isn't updated if parent is reordered + retSubCategories, err := MTOSubcategoryGetByParentIDLoader(suite.testConfigs.Context, plan.ID, cat2.ID) + suite.NoError(err) + suite.Len(retSubCategories, 2, "There should be two sub categories (including uncategorized)") + suite.EqualValues(cat2ChildCategory.ID, retSubCategories[0].ID, "Category 2 Sub should remain in position 0") + + /***** verify that a category for another model plan isn't updated + *****/ + // Verify positions after reordering for other Model Plan + retCategoriesPLan2, err := MTOCategoryGetByModelPlanIDLOADER(suite.testConfigs.Context, plan2.ID) + suite.NoError(err) + suite.Len(retCategoriesPLan2, 2) + //Verify that a child category isn't updated if parent is reordered + retSubCategoriesPlan2, err := MTOSubcategoryGetByParentIDLoader(suite.testConfigs.Context, plan2.ID, plan2Cat.ID) + suite.NoError(err) + suite.Len(retSubCategoriesPlan2, 2, "There should be two sub categories (including uncategorized)") + suite.EqualValues(plan2CatSub.ID, retSubCategoriesPlan2[0].ID, "Category 2 Sub should remain in position 0") + /*end */ +} + +// TestMTOSubCategoryReorderToPositionTwo validates that the sub categories move as expected +func (suite *ResolverSuite) TestMTOSubCategoryReorderToPositionTwo() { + plan := suite.createModelPlan("Testing Plan for Reordering") + + // Create multiple categories for model plan + cat0Name := "Category 0" + cat1Name := "Category 1" + cat2Name := "Category 2" + cat2Sub0Name := "Category 2 Sub 0" + cat2Sub1Name := "Category 2 Sub 1" + cat2Sub2Name := "Category 2 Sub 2" + names := []string{cat0Name, cat1Name, cat2Name} + categories := suite.createMultipleMTOcategories(names, plan.ID, nil) + suite.Len(categories, 3) + + cat0, cat1, cat2 := categories[0], categories[1], categories[2] + + // Make child for category 2 + subCategoryNames := []string{cat2Sub0Name, cat2Sub1Name, cat2Sub2Name} + cat2SubCategories := suite.createMultipleMTOcategories(subCategoryNames, plan.ID, &cat2.ID) + cat2Sub0, cat2Sub1, cat2Sub2 := cat2SubCategories[0], cat2SubCategories[1], cat2SubCategories[2] + + // Create a category for another model plan + plan2 := suite.createModelPlan("Testing Plan for Multiple Categories") + plan2Cat := suite.createMTOCategory("placeholder", plan2.ID, nil) + plan2CatSub := suite.createMTOCategory("placeholder sub", plan2.ID, &plan2Cat.ID) + suite.Equal(0, plan2Cat.Position) + suite.Equal(0, plan2CatSub.Position) + + // reorder the subcategory + // Move cat2Sub0 to position 2 and verify reordering + _, err := MTOCategoryReorder(suite.testConfigs.Context, suite.testConfigs.Logger, suite.testConfigs.Principal, suite.testConfigs.Store, cat2Sub0.ID, 2) + suite.NoError(err) + + retSubcategories, err := MTOSubcategoryGetByParentIDLoader(suite.testConfigs.Context, plan.ID, cat2.ID) + suite.NoError(err) + suite.EqualValues(cat2Sub1.ID, retSubcategories[0].ID, "Subcategory 1 should now be in position 0") + suite.EqualValues(cat2Sub2.ID, retSubcategories[1].ID, "Subcategory 2 should move to position 1") + suite.EqualValues(cat2Sub0.ID, retSubcategories[2].ID, "Subcategory 0 should move to position 2") + + // Verify positions after reordering + //verify parent categories are unaffected + retCategories, err := MTOCategoryGetByModelPlanIDLOADER(suite.testConfigs.Context, plan.ID) + suite.NoError(err) + suite.Len(retCategories, 4) + + suite.EqualValues(cat0.ID, retCategories[0].ID, "Category 0 should now be in position 0") + suite.EqualValues(cat1.ID, retCategories[1].ID, "Category 1 should move to position 1") + suite.EqualValues(cat2.ID, retCategories[2].ID, "Category 2 should move to position 2") + + /***** verify that a category for another model plan isn't updated + *****/ + // Verify positions after reordering for other Model Plan + retCategoriesPLan2, err := MTOCategoryGetByModelPlanIDLOADER(suite.testConfigs.Context, plan2.ID) + suite.NoError(err) + suite.Len(retCategoriesPLan2, 2) + //Verify that a child category isn't updated if parent is reordered + retSubCategoriesPlan2, err := MTOSubcategoryGetByParentIDLoader(suite.testConfigs.Context, plan2.ID, plan2Cat.ID) + suite.NoError(err) + suite.Len(retSubCategoriesPlan2, 2, "There should be two sub categories (including uncategorized)") + suite.EqualValues(plan2CatSub.ID, retSubCategoriesPlan2[0].ID, "Category 2 Sub should remain in position 0") + /*end */ +} + +// TestMTOSubCategoryReorderToPositionZero validates that the sub categories move as expected +func (suite *ResolverSuite) TestMTOSubCategoryReorderToPositionZero() { + plan := suite.createModelPlan("Testing Plan for Reordering") + + // Create multiple categories for model plan + cat0Name := "Category 0" + cat1Name := "Category 1" + cat2Name := "Category 2" + cat2Sub0Name := "Category 2 Sub 0" + cat2Sub1Name := "Category 2 Sub 1" + cat2Sub2Name := "Category 2 Sub 2" + names := []string{cat0Name, cat1Name, cat2Name} + categories := suite.createMultipleMTOcategories(names, plan.ID, nil) + suite.Len(categories, 3) + + cat0, cat1, cat2 := categories[0], categories[1], categories[2] + + // Make child for category 2 + subCategoryNames := []string{cat2Sub0Name, cat2Sub1Name, cat2Sub2Name} + cat2SubCategories := suite.createMultipleMTOcategories(subCategoryNames, plan.ID, &cat2.ID) + cat2Sub0, cat2Sub1, cat2Sub2 := cat2SubCategories[0], cat2SubCategories[1], cat2SubCategories[2] + + // Create a category for another model plan + plan2 := suite.createModelPlan("Testing Plan for Multiple Categories") + plan2Cat := suite.createMTOCategory("placeholder", plan2.ID, nil) + plan2CatSub := suite.createMTOCategory("placeholder sub", plan2.ID, &plan2Cat.ID) + suite.Equal(0, plan2Cat.Position) + suite.Equal(0, plan2CatSub.Position) + + // reorder the subcategory + // Move cat2Sub0 to position 2 and verify reordering + _, err := MTOCategoryReorder(suite.testConfigs.Context, suite.testConfigs.Logger, suite.testConfigs.Principal, suite.testConfigs.Store, cat2Sub2.ID, 0) + suite.NoError(err) + + retSubcategories, err := MTOSubcategoryGetByParentIDLoader(suite.testConfigs.Context, plan.ID, cat2.ID) + suite.NoError(err) + suite.EqualValues(cat2Sub2.ID, retSubcategories[0].ID, "Subcategory 2 should now be in position 0") + suite.EqualValues(cat2Sub0.ID, retSubcategories[1].ID, "Subcategory 0 should move to position 1") + suite.EqualValues(cat2Sub1.ID, retSubcategories[2].ID, "Subcategory 1 should move to position 2") + + // Verify positions after reordering + //verify parent categories are unaffected + retCategories, err := MTOCategoryGetByModelPlanIDLOADER(suite.testConfigs.Context, plan.ID) + suite.NoError(err) + suite.Len(retCategories, 4) + + suite.EqualValues(cat0.ID, retCategories[0].ID, "Category 0 should now be in position 0") + suite.EqualValues(cat1.ID, retCategories[1].ID, "Category 1 should move to position 1") + suite.EqualValues(cat2.ID, retCategories[2].ID, "Category 2 should move to position 2") + + /***** verify that a category for another model plan isn't updated + *****/ + // Verify positions after reordering for other Model Plan + retCategoriesPLan2, err := MTOCategoryGetByModelPlanIDLOADER(suite.testConfigs.Context, plan2.ID) + suite.NoError(err) + suite.Len(retCategoriesPLan2, 2) + //Verify that a child category isn't updated if parent is reordered + retSubCategoriesPlan2, err := MTOSubcategoryGetByParentIDLoader(suite.testConfigs.Context, plan2.ID, plan2Cat.ID) + suite.NoError(err) + suite.Len(retSubCategoriesPlan2, 2, "There should be two sub categories (including uncategorized)") + suite.EqualValues(plan2CatSub.ID, retSubCategoriesPlan2[0].ID, "Category 2 Sub should remain in position 0") + /*end */ +} diff --git a/pkg/graph/schema/types/mto_category.graphql b/pkg/graph/schema/types/mto_category.graphql index 1621720609..432c6238cd 100644 --- a/pkg/graph/schema/types/mto_category.graphql +++ b/pkg/graph/schema/types/mto_category.graphql @@ -4,6 +4,7 @@ type MTOCategory { # DB Fields id: UUID! # TODO: If we handle "Uncategorized" as a real category, maybe it won't actually have an ID? name: String! + position: Int! # Custom Resolvers @@ -15,6 +16,7 @@ type MTOSubcategory { # DB Fields id: UUID! # TODO: If we handle "Uncategorized" as a real category, maybe it won't actually have an ID? name: String! + position: Int! # Custom Resolvers isUncategorized: Boolean! @@ -32,6 +34,9 @@ extend type Mutation { Allows you to rename an MTO category. Notably, name is the only field that can be updated. You cannot have a duplicate name per model plan and parent. If the change makes a conflict, this will error. """ - updateMTOCategory(id: UUID!, name: String!): MTOCategory! + renameMTOCategory(id: UUID!, name: String!): MTOCategory! + @hasRole(role: MINT_USER) + + reorderMTOCategory(id: UUID!, newOrder: Int!): MTOCategory! @hasRole(role: MINT_USER) } \ No newline at end of file diff --git a/pkg/models/mto_category.go b/pkg/models/mto_category.go index 456cd8ef6b..0c9d915ae9 100644 --- a/pkg/models/mto_category.go +++ b/pkg/models/mto_category.go @@ -2,38 +2,78 @@ package models import ( "github.com/google/uuid" + "github.com/samber/lo" "github.com/cms-enterprise/mint-app/pkg/constants" - "github.com/cms-enterprise/mint-app/pkg/helpers" ) const unCategorizedMTOName = "Uncategorized" +type Positioner interface { + GetPosition() int +} + type MTOCategory struct { baseStruct modelPlanRelation Name string `json:"name" db:"name"` + Position int `json:"position" db:"position"` ParentID *uuid.UUID `json:"parent_id" db:"parent_id"` } +func (m MTOCategory) GetPosition() int { + return m.Position +} +func (m MTOSubcategory) GetPosition() int { + return m.Position +} + // NewMTOCategory returns a new mtoCategory object. A Nil parentID means that this is a top level category, and not a subcategory -func NewMTOCategory(createdBy uuid.UUID, name string, modelPlanID uuid.UUID, parentID *uuid.UUID) *MTOCategory { +// Note, a new category automatically is added as the last in order. It can be re-ordered, but it can't be set from the start +// We set a position simply to allow manual manipulation of position for non db- entities (uncategorized categories) +func NewMTOCategory(createdBy uuid.UUID, name string, modelPlanID uuid.UUID, parentID *uuid.UUID, position int) *MTOCategory { return &MTOCategory{ Name: name, baseStruct: NewBaseStruct(createdBy), modelPlanRelation: NewModelPlanRelation(modelPlanID), ParentID: parentID, + Position: position, + } +} + +// MTOUncategorizedFromArray takes an array of sibling categories to determine the next position that is relevant for the array. +// the new uncategorized result will now have the correct position +func MTOUncategorizedFromArray(modelPlanID uuid.UUID, parentID *uuid.UUID, categories []*MTOCategory) *MTOCategory { + //Find the greatest position, and iterate it to create the new Uncategorized category with a position as well + maxPosition := GetMaxPosition(categories) + return MTOUncategorized(modelPlanID, parentID, maxPosition+1) + +} + +// GetMaxPosition will return the max position from an array of positioner types +func GetMaxPosition[T Positioner](positioners []T) int { + if len(positioners) <= 0 { + return 0 } + maxPosition := lo.MaxBy(positioners, func(a, b T) bool { + return a.GetPosition() > b.GetPosition() + }).GetPosition() + return maxPosition } // MTOUncategorized returns a placeholder category to hold all milestones that aren't categorized into a subcategory -func MTOUncategorized(modelPlanID uuid.UUID, parentID *uuid.UUID) *MTOCategory { - return NewMTOCategory(constants.GetSystemAccountUUID(), unCategorizedMTOName, modelPlanID, parentID) +func MTOUncategorized(modelPlanID uuid.UUID, parentID *uuid.UUID, position int) *MTOCategory { + return NewMTOCategory(constants.GetSystemAccountUUID(), unCategorizedMTOName, modelPlanID, parentID, position) +} +func MTOUncategorizedSubcategory(modelPlanID uuid.UUID, parentID *uuid.UUID, position int) *MTOSubcategory { + category := NewMTOCategory(constants.GetSystemAccountUUID(), unCategorizedMTOName, modelPlanID, parentID, position) + + return category.ToSubcategory() } -func MTOUncategorizedSubcategory(modelPlanID uuid.UUID, parentID *uuid.UUID) *MTOSubcategory { - category := NewMTOCategory(constants.GetSystemAccountUUID(), unCategorizedMTOName, modelPlanID, parentID) - helpers.PointerTo(category) +func MTOUncategorizedSubcategoryFromArray(modelPlanID uuid.UUID, parentID *uuid.UUID, subCategories []*MTOSubcategory) *MTOSubcategory { + maxPosition := GetMaxPosition(subCategories) + category := NewMTOCategory(constants.GetSystemAccountUUID(), unCategorizedMTOName, modelPlanID, parentID, maxPosition+1) return category.ToSubcategory() } diff --git a/pkg/sqlqueries/SQL/mto/category/and_subcategories_get_by_model_plan_id_LOADER.sql b/pkg/sqlqueries/SQL/mto/category/and_subcategories_get_by_model_plan_id_LOADER.sql index a0c2383778..12a550b071 100644 --- a/pkg/sqlqueries/SQL/mto/category/and_subcategories_get_by_model_plan_id_LOADER.sql +++ b/pkg/sqlqueries/SQL/mto/category/and_subcategories_get_by_model_plan_id_LOADER.sql @@ -8,9 +8,11 @@ SELECT mto_category.name, mto_category.parent_id, mto_category.model_plan_id, + mto_category.position, mto_category.created_by, mto_category.created_dts, mto_category.modified_by, mto_category.modified_dts FROM mto_category -INNER JOIN QUERIED_IDS AS qIDs ON mto_category.model_plan_id = qIDs.model_plan_id; +INNER JOIN QUERIED_IDS AS qIDs ON mto_category.model_plan_id = qIDs.model_plan_id +ORDER BY mto_category.position; diff --git a/pkg/sqlqueries/SQL/mto/category/create.sql b/pkg/sqlqueries/SQL/mto/category/create.sql index 41cd98d29a..1e5ea0cdca 100644 --- a/pkg/sqlqueries/SQL/mto/category/create.sql +++ b/pkg/sqlqueries/SQL/mto/category/create.sql @@ -3,6 +3,7 @@ INSERT INTO mto_category ( name, parent_id, model_plan_id, + position, created_by ) VALUES ( @@ -10,13 +11,25 @@ VALUES ( :name, :parent_id, :model_plan_id, + COALESCE( + ( -- Place category at the bottom in order + SELECT MAX(position) + FROM mto_category + WHERE + model_plan_id = :model_plan_id + AND (parent_id = CAST(:parent_id AS UUID) OR (CAST(:parent_id AS UUID) IS NULL AND parent_id IS NULL)) + ) + 1, + 0 + ), :created_by + ) RETURNING id, name, parent_id, -model_plan_id, +model_plan_id, +position, created_by, created_dts, modified_by, diff --git a/pkg/sqlqueries/SQL/mto/category/get_by_id.sql b/pkg/sqlqueries/SQL/mto/category/get_by_id.sql index 4170accd94..ed78f4b47e 100644 --- a/pkg/sqlqueries/SQL/mto/category/get_by_id.sql +++ b/pkg/sqlqueries/SQL/mto/category/get_by_id.sql @@ -3,6 +3,7 @@ SELECT name, parent_id, model_plan_id, + position, created_by, created_dts, modified_by, diff --git a/pkg/sqlqueries/SQL/mto/category/get_by_model_plan_id_LOADER.sql b/pkg/sqlqueries/SQL/mto/category/get_by_model_plan_id_LOADER.sql index 5fb7694426..5824f5811a 100644 --- a/pkg/sqlqueries/SQL/mto/category/get_by_model_plan_id_LOADER.sql +++ b/pkg/sqlqueries/SQL/mto/category/get_by_model_plan_id_LOADER.sql @@ -8,10 +8,12 @@ SELECT mto_category.name, mto_category.parent_id, mto_category.model_plan_id, + mto_category.position, mto_category.created_by, mto_category.created_dts, mto_category.modified_by, mto_category.modified_dts FROM mto_category INNER JOIN QUERIED_IDS AS qIDs ON mto_category.model_plan_id = qIDs.model_plan_id -WHERE mto_category.parent_id IS NULL; +WHERE mto_category.parent_id IS NULL +ORDER BY mto_category.position; diff --git a/pkg/sqlqueries/SQL/mto/category/get_by_parent_id_LOADER.sql b/pkg/sqlqueries/SQL/mto/category/get_by_parent_id_LOADER.sql index b2c9d922dc..25a0d36af4 100644 --- a/pkg/sqlqueries/SQL/mto/category/get_by_parent_id_LOADER.sql +++ b/pkg/sqlqueries/SQL/mto/category/get_by_parent_id_LOADER.sql @@ -8,9 +8,11 @@ SELECT mto_category.name, mto_category.parent_id, mto_category.model_plan_id, + mto_category.position, mto_category.created_by, mto_category.created_dts, mto_category.modified_by, mto_category.modified_dts FROM mto_category -INNER JOIN QUERIED_IDS AS qIDs ON mto_category.parent_id = qIDs.parent_id; +INNER JOIN QUERIED_IDS AS qIDs ON mto_category.parent_id = qIDs.parent_id +ORDER BY mto_category.position; diff --git a/pkg/sqlqueries/SQL/mto/category/update.sql b/pkg/sqlqueries/SQL/mto/category/update.sql index 82c061c646..0a5745a171 100644 --- a/pkg/sqlqueries/SQL/mto/category/update.sql +++ b/pkg/sqlqueries/SQL/mto/category/update.sql @@ -1,6 +1,7 @@ UPDATE mto_category SET name= :name, + position= :position, modified_by= :modified_by, modified_dts= CURRENT_TIMESTAMP WHERE mto_category.id = :id @@ -9,6 +10,7 @@ id, name, parent_id, model_plan_id, +position, created_by, created_dts, modified_by, diff --git a/src/gql/generated/graphql.ts b/src/gql/generated/graphql.ts index 288c5212be..30b5abfa23 100644 --- a/src/gql/generated/graphql.ts +++ b/src/gql/generated/graphql.ts @@ -721,6 +721,7 @@ export type MtoCategory = { id: Scalars['UUID']['output']; isUncategorized: Scalars['Boolean']['output']; name: Scalars['String']['output']; + position: Scalars['Int']['output']; subCategories: Array; }; @@ -873,6 +874,7 @@ export type MtoSubcategory = { isUncategorized: Scalars['Boolean']['output']; milestones: Array; name: Scalars['String']['output']; + position: Scalars['Int']['output']; }; export enum MintUses { @@ -1115,6 +1117,12 @@ export type Mutation = { /** Marks a single notification as read. It requires that the notification be owned by the context of the user sending this request, or it will fail */ markNotificationAsRead: UserNotification; removePlanDocumentSolutionLinks: Scalars['Boolean']['output']; + /** + * Allows you to rename an MTO category. Notably, name is the only field that can be updated. + * You cannot have a duplicate name per model plan and parent. If the change makes a conflict, this will error. + */ + renameMTOCategory: MtoCategory; + reorderMTOCategory: MtoCategory; reportAProblem: Scalars['Boolean']['output']; /** This mutation sends feedback about the MINT product to the MINT team */ sendFeedbackEmail: Scalars['Boolean']['output']; @@ -1127,11 +1135,6 @@ export type Mutation = { * The fieldName allows it so you can create links for multiple sections of the model plan */ updateExistingModelLinks: ExistingModelLinks; - /** - * Allows you to rename an MTO category. Notably, name is the only field that can be updated. - * You cannot have a duplicate name per model plan and parent. If the change makes a conflict, this will error. - */ - updateMTOCategory: MtoCategory; updateMTOMilestone: MtoMilestone; updateModelPlan: ModelPlan; updateOperationalSolution: OperationalSolution; @@ -1316,6 +1319,20 @@ export type MutationRemovePlanDocumentSolutionLinksArgs = { }; +/** Mutations definition for the schema */ +export type MutationRenameMtoCategoryArgs = { + id: Scalars['UUID']['input']; + name: Scalars['String']['input']; +}; + + +/** Mutations definition for the schema */ +export type MutationReorderMtoCategoryArgs = { + id: Scalars['UUID']['input']; + newOrder: Scalars['Int']['input']; +}; + + /** Mutations definition for the schema */ export type MutationReportAProblemArgs = { input: ReportAProblemInput; @@ -1367,13 +1384,6 @@ export type MutationUpdateExistingModelLinksArgs = { }; -/** Mutations definition for the schema */ -export type MutationUpdateMtoCategoryArgs = { - id: Scalars['UUID']['input']; - name: Scalars['String']['input']; -}; - - /** Mutations definition for the schema */ export type MutationUpdateMtoMilestoneArgs = { changes: MtoMilestoneChanges;