diff --git a/api/openapi.json b/api/openapi.json index 0ddfe14..ba0399c 100644 --- a/api/openapi.json +++ b/api/openapi.json @@ -892,6 +892,17 @@ "Courses" ], "parameters": [ + { + "in": "query", + "name": "locale", + "schema": { + "type": "string", + "enum": [ + "en-us", + "zh-cn" + ] + } + }, { "in": "query", "name": "search", @@ -1519,6 +1530,9 @@ } } }, + "413": { + "description": "Response 413" + }, "429": { "description": "Error response", "headers": { @@ -1708,6 +1722,17 @@ "Schedules" ], "parameters": [ + { + "in": "query", + "name": "locale", + "schema": { + "type": "string", + "enum": [ + "en-us", + "zh-cn" + ] + } + }, { "in": "query", "name": "sectionId", @@ -1856,6 +1881,17 @@ "Sections" ], "parameters": [ + { + "in": "query", + "name": "locale", + "schema": { + "type": "string", + "enum": [ + "en-us", + "zh-cn" + ] + } + }, { "in": "query", "name": "courseId", @@ -2069,6 +2105,17 @@ "Teachers" ], "parameters": [ + { + "in": "query", + "name": "locale", + "schema": { + "type": "string", + "enum": [ + "en-us", + "zh-cn" + ] + } + }, { "in": "query", "name": "departmentId", @@ -4160,6 +4207,17 @@ "format": "int64" }, "required": true + }, + { + "in": "query", + "name": "locale", + "schema": { + "type": "string", + "enum": [ + "en-us", + "zh-cn" + ] + } } ], "responses": { @@ -4815,6 +4873,17 @@ "format": "int64" }, "required": true + }, + { + "in": "query", + "name": "locale", + "schema": { + "type": "string", + "enum": [ + "en-us", + "zh-cn" + ] + } } ], "responses": { @@ -4991,6 +5060,17 @@ "format": "int64" }, "required": true + }, + { + "in": "query", + "name": "locale", + "schema": { + "type": "string", + "enum": [ + "en-us", + "zh-cn" + ] + } } ], "responses": { @@ -7677,6 +7757,17 @@ "format": "int64" }, "required": true + }, + { + "in": "query", + "name": "locale", + "schema": { + "type": "string", + "enum": [ + "en-us", + "zh-cn" + ] + } } ], "responses": { @@ -7723,6 +7814,17 @@ }, "required": true }, + { + "in": "query", + "name": "locale", + "schema": { + "type": "string", + "enum": [ + "en-us", + "zh-cn" + ] + } + }, { "in": "query", "name": "dateFrom", @@ -7947,6 +8049,9 @@ "body": { "type": "string" }, + "renderedBody": { + "type": "string" + }, "visibility": { "type": "string" }, @@ -8098,6 +8203,7 @@ "required": [ "id", "body", + "renderedBody", "visibility", "status", "author", @@ -8168,6 +8274,9 @@ "body": { "type": "string" }, + "renderedBody": { + "type": "string" + }, "visibility": { "type": "string" }, @@ -8322,6 +8431,7 @@ "required": [ "id", "body", + "renderedBody", "visibility", "status", "author", @@ -8840,6 +8950,18 @@ "minimum": -9007199254740991, "maximum": 9007199254740991 }, + "sourceLastSeenAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "retiredAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, "code": { "type": "string" }, @@ -9545,6 +9667,18 @@ "minimum": -9007199254740991, "maximum": 9007199254740991 }, + "sourceLastSeenAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "retiredAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, "code": { "type": "string" }, @@ -10262,6 +10396,18 @@ "minimum": -9007199254740991, "maximum": 9007199254740991 }, + "sourceLastSeenAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "retiredAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, "code": { "type": "string" }, @@ -11617,6 +11763,9 @@ "format": "date-time", "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" }, + "renderedHtml": { + "type": "string" + }, "lastEditedBy": { "nullable": true, "type": "object", @@ -11651,6 +11800,7 @@ "content", "updatedAt", "lastEditedAt", + "renderedHtml", "lastEditedBy" ], "additionalProperties": false @@ -12046,6 +12196,18 @@ "minimum": -9007199254740991, "maximum": 9007199254740991 }, + "sourceLastSeenAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "retiredAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, "code": { "type": "string" }, @@ -12881,6 +13043,18 @@ "minimum": -9007199254740991, "maximum": 9007199254740991 }, + "sourceLastSeenAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "retiredAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, "code": { "type": "string" }, @@ -14283,6 +14457,18 @@ "minimum": -9007199254740991, "maximum": 9007199254740991 }, + "sourceLastSeenAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "retiredAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, "code": { "type": "string" }, @@ -17662,6 +17848,18 @@ "minimum": -9007199254740991, "maximum": 9007199254740991 }, + "sourceLastSeenAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "retiredAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, "code": { "type": "string" }, @@ -18349,6 +18547,18 @@ "minimum": -9007199254740991, "maximum": 9007199254740991 }, + "sourceLastSeenAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "retiredAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, "code": { "type": "string" }, @@ -19048,6 +19258,18 @@ "minimum": -9007199254740991, "maximum": 9007199254740991 }, + "sourceLastSeenAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "retiredAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, "code": { "type": "string" }, @@ -19803,6 +20025,18 @@ "minimum": -9007199254740991, "maximum": 9007199254740991 }, + "sourceLastSeenAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "retiredAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, "code": { "type": "string" }, @@ -20460,6 +20694,18 @@ "minimum": -9007199254740991, "maximum": 9007199254740991 }, + "sourceLastSeenAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "retiredAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, "code": { "type": "string" }, @@ -21117,6 +21363,18 @@ "minimum": -9007199254740991, "maximum": 9007199254740991 }, + "sourceLastSeenAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "retiredAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, "code": { "type": "string" }, @@ -21776,6 +22034,18 @@ "minimum": -9007199254740991, "maximum": 9007199254740991 }, + "sourceLastSeenAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "retiredAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, "code": { "type": "string" }, @@ -22562,6 +22832,18 @@ "minimum": -9007199254740991, "maximum": 9007199254740991 }, + "sourceLastSeenAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "retiredAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, "code": { "type": "string" }, @@ -23780,6 +24062,18 @@ "minimum": -9007199254740991, "maximum": 9007199254740991 }, + "sourceLastSeenAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "retiredAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, "code": { "type": "string" }, @@ -24355,6 +24649,18 @@ "minimum": -9007199254740991, "maximum": 9007199254740991 }, + "sourceLastSeenAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "retiredAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, "code": { "type": "string" }, @@ -25579,6 +25885,18 @@ "minimum": -9007199254740991, "maximum": 9007199254740991 }, + "sourceLastSeenAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "retiredAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, "code": { "type": "string" }, @@ -26188,6 +26506,18 @@ "minimum": -9007199254740991, "maximum": 9007199254740991 }, + "sourceLastSeenAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "retiredAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, "code": { "type": "string" }, @@ -26817,6 +27147,18 @@ "minimum": -9007199254740991, "maximum": 9007199254740991 }, + "sourceLastSeenAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "retiredAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, "code": { "type": "string" }, @@ -27155,6 +27497,18 @@ "minimum": -9007199254740991, "maximum": 9007199254740991 }, + "sourceLastSeenAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "retiredAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, "code": { "type": "string" }, @@ -28554,6 +28908,18 @@ "minimum": -9007199254740991, "maximum": 9007199254740991 }, + "sourceLastSeenAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "retiredAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, "code": { "type": "string" }, @@ -29371,6 +29737,18 @@ "minimum": -9007199254740991, "maximum": 9007199254740991 }, + "sourceLastSeenAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "retiredAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, "code": { "type": "string" }, @@ -31380,6 +31758,18 @@ "minimum": -9007199254740991, "maximum": 9007199254740991 }, + "sourceLastSeenAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "retiredAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, "code": { "type": "string" }, @@ -32433,6 +32823,18 @@ "minimum": -9007199254740991, "maximum": 9007199254740991 }, + "sourceLastSeenAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, + "retiredAt": { + "nullable": true, + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + }, "code": { "type": "string" }, diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 0e80d6b..b31daa9 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -250,6 +250,10 @@ func openBrowser(url string) error { return cmd.Start() } +func callbackRedirectURI(addr net.Addr) string { + return fmt.Sprintf("http://%s/callback", addr.String()) +} + // Login runs the full OAuth2 Authorization Code + PKCE flow. // Returns a credential to store. func Login(server string) (*config.Credential, error) { @@ -274,8 +278,7 @@ func Login(server string) (*config.Credential, error) { if err != nil { return nil, err } - port := listener.Addr().(*net.TCPAddr).Port - redirectURI := fmt.Sprintf("http://localhost:%d/callback", port) + redirectURI := callbackRedirectURI(listener.Addr()) // Register client clientInfo, err := registerClient(regEndpoint, redirectURI) diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index 30f16c5..ae0e23f 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -3,6 +3,9 @@ package auth import ( "crypto/rand" "crypto/rsa" + "net" + "net/url" + "strconv" "testing" "time" @@ -10,6 +13,30 @@ import ( "github.com/go-jose/go-jose/v4/jwt" ) +func TestCallbackRedirectURIMatchesLoopbackListener(t *testing.T) { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + _ = listener.Close() + }) + + redirect, err := url.Parse(callbackRedirectURI(listener.Addr())) + if err != nil { + t.Fatal(err) + } + if redirect.Hostname() != "127.0.0.1" { + t.Fatalf("redirect hostname = %q, want 127.0.0.1", redirect.Hostname()) + } + if redirect.Port() != strconv.Itoa(listener.Addr().(*net.TCPAddr).Port) { + t.Fatalf("redirect port = %q, listener = %q", redirect.Port(), listener.Addr()) + } + if redirect.Path != "/callback" { + t.Fatalf("redirect path = %q, want /callback", redirect.Path) + } +} + func TestVerifiedTokenToCredentialUsesFallbacks(t *testing.T) { vt := &VerifiedToken{ AccessToken: "access", diff --git a/internal/cmd/course/course.go b/internal/cmd/course/course.go index 511fd9d..c21ea38 100644 --- a/internal/cmd/course/course.go +++ b/internal/cmd/course/course.go @@ -85,6 +85,9 @@ func runCourseList(cmd *cobra.Command, opts courseListOpts) error { } return tui.TableResult{Rows: list.Rows, Total: list.Total, Page: list.Page}, nil }, + OnSelect: func(row map[string]any) error { + return runCourseView(cmd, fmt.Sprint(row["jwId"])) + }, EmptyMessage: "No courses found. Try a broader search.", }) } @@ -126,7 +129,7 @@ func courseListColumns() []output.Column { {Header: "Code", Key: "code"}, {Header: "Name", Key: "namePrimary"}, {Header: "Level", Key: "educationLevel.name"}, - {Header: "ID", Key: "id"}, + {Header: "JW ID", Key: "jwId"}, } } @@ -161,46 +164,50 @@ func newCmdView() *cobra.Command { Short: "View course details", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - c, err := api.NewTypedClient(cmdutil.ServerFromCmd(cmd), false) - if err != nil { - return err - } - jwID, err := cmdutil.Int64PtrIfSet(args[0]) - if err != nil { - return err - } - data, err := api.ParseResponseRaw(c.GetCourse(api.Ctx(), *jwID)) - if err != nil { - return err - } - if output.IsJSON() { - return output.JSON(data) - } - m := cmdutil.AsMap(data) - output.KVWithTitle([]output.KVPair{ - {Key: "ID", Value: output.Resolve(m, "id")}, - {Key: "Code", Value: output.Resolve(m, "code")}, - {Key: "Name", Value: output.Resolve(m, "namePrimary")}, - {Key: "Name (EN)", Value: output.Resolve(m, "nameSecondary")}, - {Key: "Level", Value: output.Resolve(m, "educationLevel.name")}, - {Key: "Category", Value: output.Resolve(m, "category.name")}, - {Key: "Class type", Value: output.Resolve(m, "classType.name")}, - {Key: "Gradation", Value: output.Resolve(m, "gradation.name")}, - {Key: "Course type", Value: output.Resolve(m, "type.name")}, - }, "Course") - - if sections, ok := m["sections"].([]any); ok && len(sections) > 0 { - fmt.Println() - output.Bold(" Sections") - rows := cmdutil.RowsFromAny(sections) - output.Table(rows, []output.Column{ - {Header: "ID", Key: "id"}, - {Header: "Code", Key: "code"}, - {Header: "Semester", Key: "semester.name"}, - {Header: "Campus", Key: "campus.name"}, - }) - } - return nil + return runCourseView(cmd, args[0]) }, } } + +func runCourseView(cmd *cobra.Command, id string) error { + c, err := api.NewTypedClient(cmdutil.ServerFromCmd(cmd), false) + if err != nil { + return err + } + jwID, err := cmdutil.Int64PtrIfSet(id) + if err != nil { + return err + } + data, err := api.ParseResponseRaw(c.GetCourse(api.Ctx(), *jwID, nil)) + if err != nil { + return err + } + if output.IsJSON() { + return output.JSON(data) + } + m := cmdutil.AsMap(data) + output.KVWithTitle([]output.KVPair{ + {Key: "ID", Value: output.Resolve(m, "id")}, + {Key: "Code", Value: output.Resolve(m, "code")}, + {Key: "Name", Value: output.Resolve(m, "namePrimary")}, + {Key: "Name (EN)", Value: output.Resolve(m, "nameSecondary")}, + {Key: "Level", Value: output.Resolve(m, "educationLevel.name")}, + {Key: "Category", Value: output.Resolve(m, "category.name")}, + {Key: "Class type", Value: output.Resolve(m, "classType.name")}, + {Key: "Gradation", Value: output.Resolve(m, "gradation.name")}, + {Key: "Course type", Value: output.Resolve(m, "type.name")}, + }, "Course") + + if sections, ok := m["sections"].([]any); ok && len(sections) > 0 { + fmt.Println() + output.Bold(" Sections") + rows := cmdutil.RowsFromAny(sections) + output.Table(rows, []output.Column{ + {Header: "ID", Key: "id"}, + {Header: "Code", Key: "code"}, + {Header: "Semester", Key: "semester.name"}, + {Header: "Campus", Key: "campus.name"}, + }) + } + return nil +} diff --git a/internal/cmd/course/course_test.go b/internal/cmd/course/course_test.go new file mode 100644 index 0000000..f16b0ef --- /dev/null +++ b/internal/cmd/course/course_test.go @@ -0,0 +1,11 @@ +package course + +import "testing" + +func TestCourseListUsesViewIdentifier(t *testing.T) { + columns := courseListColumns() + last := columns[len(columns)-1] + if last.Header != "JW ID" || last.Key != "jwId" { + t.Fatalf("last column = %#v, want JW ID backed by jwId", last) + } +} diff --git a/internal/cmd/section/section.go b/internal/cmd/section/section.go index 6ae2f8e..c98bb2f 100644 --- a/internal/cmd/section/section.go +++ b/internal/cmd/section/section.go @@ -107,6 +107,9 @@ func runSectionList(cmd *cobra.Command, opts sectionListOpts) error { } return tui.TableResult{Rows: list.Rows, Total: list.Total, Page: list.Page}, nil }, + OnSelect: func(row map[string]any) error { + return runSectionView(cmd, fmt.Sprint(row["jwId"])) + }, EmptyMessage: "No sections found. Try a broader search.", }) } @@ -156,7 +159,7 @@ func sectionListColumns() []output.Column { {Header: "Course", Key: "course.namePrimary"}, {Header: "Semester", Key: "semester.name"}, {Header: "Campus", Key: "campus.name"}, - {Header: "ID", Key: "id"}, + {Header: "JW ID", Key: "jwId"}, } } @@ -194,60 +197,64 @@ func newCmdView() *cobra.Command { Short: "View section details", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - c, err := api.NewTypedClient(cmdutil.ServerFromCmd(cmd), false) - if err != nil { - return err - } - jwID, err := cmdutil.Int64PtrIfSet(args[0]) - if err != nil { - return err - } - data, err := api.ParseResponseRaw(c.GetSection(api.Ctx(), *jwID)) - if err != nil { - return err - } - if output.IsJSON() { - return output.JSON(data) - } - m := cmdutil.AsMap(data) - output.KVWithTitle([]output.KVPair{ - {Key: "ID", Value: output.Resolve(m, "id")}, - {Key: "Code", Value: output.Resolve(m, "code")}, - {Key: "Course", Value: output.Resolve(m, "course.namePrimary")}, - {Key: "Semester", Value: output.Resolve(m, "semester.name")}, - {Key: "Campus", Value: output.Resolve(m, "campus.name")}, - }, "Section") + return runSectionView(cmd, args[0]) + }, + } +} - if teachers, ok := m["teachers"].([]any); ok && len(teachers) > 0 { - fmt.Println() - output.Bold(" Teachers") - rows := cmdutil.RowsFromAny(teachers) - output.Table(rows, []output.Column{ - {Header: "ID", Key: "id"}, - {Header: "Name", Key: "namePrimary"}, - {Header: "Name (EN)", Key: "nameSecondary"}, - {Header: "Department", Key: "department.name"}, - }) - } +func runSectionView(cmd *cobra.Command, id string) error { + c, err := api.NewTypedClient(cmdutil.ServerFromCmd(cmd), false) + if err != nil { + return err + } + jwID, err := cmdutil.Int64PtrIfSet(id) + if err != nil { + return err + } + data, err := api.ParseResponseRaw(c.GetSection(api.Ctx(), *jwID, nil)) + if err != nil { + return err + } + if output.IsJSON() { + return output.JSON(data) + } + m := cmdutil.AsMap(data) + output.KVWithTitle([]output.KVPair{ + {Key: "ID", Value: output.Resolve(m, "id")}, + {Key: "Code", Value: output.Resolve(m, "code")}, + {Key: "Course", Value: output.Resolve(m, "course.namePrimary")}, + {Key: "Semester", Value: output.Resolve(m, "semester.name")}, + {Key: "Campus", Value: output.Resolve(m, "campus.name")}, + }, "Section") - if schedules, ok := m["schedules"].([]any); ok && len(schedules) > 0 { - fmt.Println() - output.Bold(" Schedules") - rows := cmdutil.RowsFromAny(schedules) - for _, row := range rows { - normalizeScheduleRow(row) - } - output.Table(rows, []output.Column{ - {Header: "ID", Key: "id"}, - {Header: "Day", Key: "weekday"}, - {Header: "Start", Key: "startTime"}, - {Header: "End", Key: "endTime"}, - {Header: "Place", Key: "customPlace"}, - }) - } - return nil - }, + if teachers, ok := m["teachers"].([]any); ok && len(teachers) > 0 { + fmt.Println() + output.Bold(" Teachers") + rows := cmdutil.RowsFromAny(teachers) + output.Table(rows, []output.Column{ + {Header: "ID", Key: "id"}, + {Header: "Name", Key: "namePrimary"}, + {Header: "Name (EN)", Key: "nameSecondary"}, + {Header: "Department", Key: "department.name"}, + }) + } + + if schedules, ok := m["schedules"].([]any); ok && len(schedules) > 0 { + fmt.Println() + output.Bold(" Schedules") + rows := cmdutil.RowsFromAny(schedules) + for _, row := range rows { + normalizeScheduleRow(row) + } + output.Table(rows, []output.Column{ + {Header: "ID", Key: "id"}, + {Header: "Day", Key: "weekday"}, + {Header: "Start", Key: "startTime"}, + {Header: "End", Key: "endTime"}, + {Header: "Place", Key: "customPlace"}, + }) } + return nil } func newCmdSchedules() *cobra.Command { diff --git a/internal/cmd/section/section_test.go b/internal/cmd/section/section_test.go index 0624475..83fd6dc 100644 --- a/internal/cmd/section/section_test.go +++ b/internal/cmd/section/section_test.go @@ -119,3 +119,11 @@ func TestNormalizeScheduleRow_FullRow(t *testing.T) { t.Errorf("endTime: got %v, want 10:40", got) } } + +func TestSectionListUsesViewIdentifier(t *testing.T) { + columns := sectionListColumns() + last := columns[len(columns)-1] + if last.Header != "JW ID" || last.Key != "jwId" { + t.Fatalf("last column = %#v, want JW ID backed by jwId", last) + } +} diff --git a/internal/cmd/teacher/teacher.go b/internal/cmd/teacher/teacher.go index 53b2944..764b844 100644 --- a/internal/cmd/teacher/teacher.go +++ b/internal/cmd/teacher/teacher.go @@ -84,6 +84,9 @@ func runTeacherList(cmd *cobra.Command, opts teacherListOpts) error { } return tui.TableResult{Rows: list.Rows, Total: list.Total, Page: list.Page}, nil }, + OnSelect: func(row map[string]any) error { + return runTeacherView(cmd, fmt.Sprint(row["id"])) + }, EmptyMessage: "No teachers found. Try a broader search.", }) } @@ -152,43 +155,47 @@ func newCmdView() *cobra.Command { Short: "View teacher details", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - c, err := api.NewTypedClient(cmdutil.ServerFromCmd(cmd), false) - if err != nil { - return err - } - teacherID, err := strconv.ParseInt(args[0], 10, 64) - if err != nil { - return fmt.Errorf("invalid teacher id %q: %w", args[0], err) - } - data, err := api.ParseResponseRaw(c.GetTeacher(api.Ctx(), teacherID)) - if err != nil { - return err - } - if output.IsJSON() { - return output.JSON(data) - } - m := cmdutil.AsMap(data) - output.KVWithTitle([]output.KVPair{ - {Key: "ID", Value: output.Resolve(m, "id")}, - {Key: "Code", Value: output.Resolve(m, "code")}, - {Key: "Name", Value: output.Resolve(m, "namePrimary")}, - {Key: "Name (EN)", Value: output.Resolve(m, "nameSecondary")}, - {Key: "Department", Value: output.Resolve(m, "department.name")}, - {Key: "Title", Value: output.Resolve(m, "title")}, - }, "Teacher") - - if sections, ok := m["sections"].([]any); ok && len(sections) > 0 { - fmt.Println() - output.Bold(" Sections") - rows := cmdutil.RowsFromAny(sections) - output.Table(rows, []output.Column{ - {Header: "ID", Key: "id"}, - {Header: "Code", Key: "code"}, - {Header: "Course", Key: "course.namePrimary"}, - {Header: "Semester", Key: "semester.name"}, - }) - } - return nil + return runTeacherView(cmd, args[0]) }, } } + +func runTeacherView(cmd *cobra.Command, id string) error { + c, err := api.NewTypedClient(cmdutil.ServerFromCmd(cmd), false) + if err != nil { + return err + } + teacherID, err := strconv.ParseInt(id, 10, 64) + if err != nil { + return fmt.Errorf("invalid teacher id %q: %w", id, err) + } + data, err := api.ParseResponseRaw(c.GetTeacher(api.Ctx(), teacherID, nil)) + if err != nil { + return err + } + if output.IsJSON() { + return output.JSON(data) + } + m := cmdutil.AsMap(data) + output.KVWithTitle([]output.KVPair{ + {Key: "ID", Value: output.Resolve(m, "id")}, + {Key: "Code", Value: output.Resolve(m, "code")}, + {Key: "Name", Value: output.Resolve(m, "namePrimary")}, + {Key: "Name (EN)", Value: output.Resolve(m, "nameSecondary")}, + {Key: "Department", Value: output.Resolve(m, "department.name")}, + {Key: "Title", Value: output.Resolve(m, "title")}, + }, "Teacher") + + if sections, ok := m["sections"].([]any); ok && len(sections) > 0 { + fmt.Println() + output.Bold(" Sections") + rows := cmdutil.RowsFromAny(sections) + output.Table(rows, []output.Column{ + {Header: "ID", Key: "id"}, + {Header: "Code", Key: "code"}, + {Header: "Course", Key: "course.namePrimary"}, + {Header: "Semester", Key: "semester.name"}, + }) + } + return nil +} diff --git a/internal/openapi/client.gen.go b/internal/openapi/client.gen.go index 3cbc394..4519a3b 100644 --- a/internal/openapi/client.gen.go +++ b/internal/openapi/client.gen.go @@ -1379,6 +1379,42 @@ func (e RemoveCommentReactionParamsType) Valid() bool { } } +// Defines values for ListCoursesParamsLocale. +const ( + ListCoursesParamsLocaleEnUs ListCoursesParamsLocale = "en-us" + ListCoursesParamsLocaleZhCn ListCoursesParamsLocale = "zh-cn" +) + +// Valid indicates whether the value is a known member of the ListCoursesParamsLocale enum. +func (e ListCoursesParamsLocale) Valid() bool { + switch e { + case ListCoursesParamsLocaleEnUs: + return true + case ListCoursesParamsLocaleZhCn: + return true + default: + return false + } +} + +// Defines values for GetCourseParamsLocale. +const ( + GetCourseParamsLocaleEnUs GetCourseParamsLocale = "en-us" + GetCourseParamsLocaleZhCn GetCourseParamsLocale = "zh-cn" +) + +// Valid indicates whether the value is a known member of the GetCourseParamsLocale enum. +func (e GetCourseParamsLocale) Valid() bool { + switch e { + case GetCourseParamsLocaleEnUs: + return true + case GetCourseParamsLocaleZhCn: + return true + default: + return false + } +} + // Defines values for GetDescriptionParamsTargetType. const ( GetDescriptionParamsTargetTypeCourse GetDescriptionParamsTargetType = "course" @@ -1457,6 +1493,132 @@ func (e GetApiMeSubscriptionsSchedulesParamsLocale) Valid() bool { } } +// Defines values for ListSchedulesParamsLocale. +const ( + ListSchedulesParamsLocaleEnUs ListSchedulesParamsLocale = "en-us" + ListSchedulesParamsLocaleZhCn ListSchedulesParamsLocale = "zh-cn" +) + +// Valid indicates whether the value is a known member of the ListSchedulesParamsLocale enum. +func (e ListSchedulesParamsLocale) Valid() bool { + switch e { + case ListSchedulesParamsLocaleEnUs: + return true + case ListSchedulesParamsLocaleZhCn: + return true + default: + return false + } +} + +// Defines values for ListSectionsParamsLocale. +const ( + ListSectionsParamsLocaleEnUs ListSectionsParamsLocale = "en-us" + ListSectionsParamsLocaleZhCn ListSectionsParamsLocale = "zh-cn" +) + +// Valid indicates whether the value is a known member of the ListSectionsParamsLocale enum. +func (e ListSectionsParamsLocale) Valid() bool { + switch e { + case ListSectionsParamsLocaleEnUs: + return true + case ListSectionsParamsLocaleZhCn: + return true + default: + return false + } +} + +// Defines values for GetSectionParamsLocale. +const ( + GetSectionParamsLocaleEnUs GetSectionParamsLocale = "en-us" + GetSectionParamsLocaleZhCn GetSectionParamsLocale = "zh-cn" +) + +// Valid indicates whether the value is a known member of the GetSectionParamsLocale enum. +func (e GetSectionParamsLocale) Valid() bool { + switch e { + case GetSectionParamsLocaleEnUs: + return true + case GetSectionParamsLocaleZhCn: + return true + default: + return false + } +} + +// Defines values for GetSectionScheduleGroupsParamsLocale. +const ( + GetSectionScheduleGroupsParamsLocaleEnUs GetSectionScheduleGroupsParamsLocale = "en-us" + GetSectionScheduleGroupsParamsLocaleZhCn GetSectionScheduleGroupsParamsLocale = "zh-cn" +) + +// Valid indicates whether the value is a known member of the GetSectionScheduleGroupsParamsLocale enum. +func (e GetSectionScheduleGroupsParamsLocale) Valid() bool { + switch e { + case GetSectionScheduleGroupsParamsLocaleEnUs: + return true + case GetSectionScheduleGroupsParamsLocaleZhCn: + return true + default: + return false + } +} + +// Defines values for GetSectionSchedulesParamsLocale. +const ( + GetSectionSchedulesParamsLocaleEnUs GetSectionSchedulesParamsLocale = "en-us" + GetSectionSchedulesParamsLocaleZhCn GetSectionSchedulesParamsLocale = "zh-cn" +) + +// Valid indicates whether the value is a known member of the GetSectionSchedulesParamsLocale enum. +func (e GetSectionSchedulesParamsLocale) Valid() bool { + switch e { + case GetSectionSchedulesParamsLocaleEnUs: + return true + case GetSectionSchedulesParamsLocaleZhCn: + return true + default: + return false + } +} + +// Defines values for ListTeachersParamsLocale. +const ( + ListTeachersParamsLocaleEnUs ListTeachersParamsLocale = "en-us" + ListTeachersParamsLocaleZhCn ListTeachersParamsLocale = "zh-cn" +) + +// Valid indicates whether the value is a known member of the ListTeachersParamsLocale enum. +func (e ListTeachersParamsLocale) Valid() bool { + switch e { + case ListTeachersParamsLocaleEnUs: + return true + case ListTeachersParamsLocaleZhCn: + return true + default: + return false + } +} + +// Defines values for GetTeacherParamsLocale. +const ( + EnUs GetTeacherParamsLocale = "en-us" + ZhCn GetTeacherParamsLocale = "zh-cn" +) + +// Valid indicates whether the value is a known member of the GetTeacherParamsLocale enum. +func (e GetTeacherParamsLocale) Valid() bool { + switch e { + case EnUs: + return true + case ZhCn: + return true + default: + return false + } +} + // Defines values for ListTodosParamsCompleted. const ( False ListTodosParamsCompleted = "false" @@ -1531,11 +1693,12 @@ type UnderscoreUnderscoreSchema0 struct { Type string `json:"type"` ViewerHasReacted bool `json:"viewerHasReacted"` } `json:"reactions"` - Replies []UnderscoreUnderscoreSchema0 `json:"replies"` - RootId *string `json:"rootId"` - Status string `json:"status"` - UpdatedAt time.Time `json:"updatedAt"` - Visibility string `json:"visibility"` + RenderedBody string `json:"renderedBody"` + Replies []UnderscoreUnderscoreSchema0 `json:"replies"` + RootId *string `json:"rootId"` + Status string `json:"status"` + UpdatedAt time.Time `json:"updatedAt"` + Visibility string `json:"visibility"` } // UnderscoreUnderscoreSchema1 defines model for __schema1. @@ -1573,11 +1736,12 @@ type UnderscoreUnderscoreSchema1 struct { Type string `json:"type"` ViewerHasReacted bool `json:"viewerHasReacted"` } `json:"reactions"` - Replies []UnderscoreUnderscoreSchema1 `json:"replies"` - RootId *string `json:"rootId"` - Status string `json:"status"` - UpdatedAt time.Time `json:"updatedAt"` - Visibility string `json:"visibility"` + RenderedBody string `json:"renderedBody"` + Replies []UnderscoreUnderscoreSchema1 `json:"replies"` + RootId *string `json:"rootId"` + Status string `json:"status"` + UpdatedAt time.Time `json:"updatedAt"` + Visibility string `json:"visibility"` } // AdminCommentsResponseSchema defines model for adminCommentsResponseSchema. @@ -2361,6 +2525,7 @@ type CalendarSubscriptionAppendResponseSchema struct { PeriodsPerWeek *int `json:"periodsPerWeek"` PracticePeriods *float32 `json:"practicePeriods"` Remark *string `json:"remark"` + RetiredAt *time.Time `json:"retiredAt,omitempty"` RoomTypeId *int `json:"roomTypeId"` ScheduleJsonParams interface{} `json:"scheduleJsonParams"` ScheduleRemark *string `json:"scheduleRemark"` @@ -2368,6 +2533,7 @@ type CalendarSubscriptionAppendResponseSchema struct { SelectedStdCount *int `json:"selectedStdCount"` Semester *SemesterSchema `json:"semester"` SemesterId *int `json:"semesterId"` + SourceLastSeenAt *time.Time `json:"sourceLastSeenAt,omitempty"` StdCount *int `json:"stdCount"` SuggestScheduleWeekInfo *string `json:"suggestScheduleWeekInfo"` SuggestScheduleWeeks interface{} `json:"suggestScheduleWeeks"` @@ -2514,6 +2680,7 @@ type CalendarSubscriptionBatchResponseSchema struct { PeriodsPerWeek *int `json:"periodsPerWeek"` PracticePeriods *float32 `json:"practicePeriods"` Remark *string `json:"remark"` + RetiredAt *time.Time `json:"retiredAt,omitempty"` RoomTypeId *int `json:"roomTypeId"` ScheduleJsonParams interface{} `json:"scheduleJsonParams"` ScheduleRemark *string `json:"scheduleRemark"` @@ -2521,6 +2688,7 @@ type CalendarSubscriptionBatchResponseSchema struct { SelectedStdCount *int `json:"selectedStdCount"` Semester *SemesterSchema `json:"semester"` SemesterId *int `json:"semesterId"` + SourceLastSeenAt *time.Time `json:"sourceLastSeenAt,omitempty"` StdCount *int `json:"stdCount"` SuggestScheduleWeekInfo *string `json:"suggestScheduleWeekInfo"` SuggestScheduleWeeks interface{} `json:"suggestScheduleWeeks"` @@ -2643,6 +2811,7 @@ type CalendarSubscriptionBatchResponseSchema struct { PeriodsPerWeek *int `json:"periodsPerWeek"` PracticePeriods *float32 `json:"practicePeriods"` Remark *string `json:"remark"` + RetiredAt *time.Time `json:"retiredAt,omitempty"` RoomTypeId *int `json:"roomTypeId"` ScheduleJsonParams interface{} `json:"scheduleJsonParams"` ScheduleRemark *string `json:"scheduleRemark"` @@ -2650,6 +2819,7 @@ type CalendarSubscriptionBatchResponseSchema struct { SelectedStdCount *int `json:"selectedStdCount"` Semester *SemesterSchema `json:"semester"` SemesterId *int `json:"semesterId"` + SourceLastSeenAt *time.Time `json:"sourceLastSeenAt,omitempty"` StdCount *int `json:"stdCount"` SuggestScheduleWeekInfo *string `json:"suggestScheduleWeekInfo"` SuggestScheduleWeeks interface{} `json:"suggestScheduleWeeks"` @@ -2786,6 +2956,7 @@ type CalendarSubscriptionCreateResponseSchema struct { PeriodsPerWeek *int `json:"periodsPerWeek"` PracticePeriods *float32 `json:"practicePeriods"` Remark *string `json:"remark"` + RetiredAt *time.Time `json:"retiredAt,omitempty"` RoomTypeId *int `json:"roomTypeId"` ScheduleJsonParams interface{} `json:"scheduleJsonParams"` ScheduleRemark *string `json:"scheduleRemark"` @@ -2793,6 +2964,7 @@ type CalendarSubscriptionCreateResponseSchema struct { SelectedStdCount *int `json:"selectedStdCount"` Semester *SemesterSchema `json:"semester"` SemesterId *int `json:"semesterId"` + SourceLastSeenAt *time.Time `json:"sourceLastSeenAt,omitempty"` StdCount *int `json:"stdCount"` SuggestScheduleWeekInfo *string `json:"suggestScheduleWeekInfo"` SuggestScheduleWeeks interface{} `json:"suggestScheduleWeeks"` @@ -2913,6 +3085,7 @@ type CalendarSubscriptionImportResponseSchema struct { PeriodsPerWeek *int `json:"periodsPerWeek"` PracticePeriods *float32 `json:"practicePeriods"` Remark *string `json:"remark"` + RetiredAt *time.Time `json:"retiredAt,omitempty"` RoomTypeId *int `json:"roomTypeId"` ScheduleJsonParams interface{} `json:"scheduleJsonParams"` ScheduleRemark *string `json:"scheduleRemark"` @@ -2920,6 +3093,7 @@ type CalendarSubscriptionImportResponseSchema struct { SelectedStdCount *int `json:"selectedStdCount"` Semester *SemesterSchema `json:"semester"` SemesterId *int `json:"semesterId"` + SourceLastSeenAt *time.Time `json:"sourceLastSeenAt,omitempty"` StdCount *int `json:"stdCount"` SuggestScheduleWeekInfo *string `json:"suggestScheduleWeekInfo"` SuggestScheduleWeeks interface{} `json:"suggestScheduleWeeks"` @@ -3034,6 +3208,7 @@ type CalendarSubscriptionImportResponseSchema struct { PeriodsPerWeek *int `json:"periodsPerWeek"` PracticePeriods *float32 `json:"practicePeriods"` Remark *string `json:"remark"` + RetiredAt *time.Time `json:"retiredAt,omitempty"` RoomTypeId *int `json:"roomTypeId"` ScheduleJsonParams interface{} `json:"scheduleJsonParams"` ScheduleRemark *string `json:"scheduleRemark"` @@ -3041,6 +3216,7 @@ type CalendarSubscriptionImportResponseSchema struct { SelectedStdCount *int `json:"selectedStdCount"` Semester *SemesterSchema `json:"semester"` SemesterId *int `json:"semesterId"` + SourceLastSeenAt *time.Time `json:"sourceLastSeenAt,omitempty"` StdCount *int `json:"stdCount"` SuggestScheduleWeekInfo *string `json:"suggestScheduleWeekInfo"` SuggestScheduleWeeks interface{} `json:"suggestScheduleWeeks"` @@ -3156,6 +3332,7 @@ type CalendarSubscriptionImportResponseSchema struct { PeriodsPerWeek *int `json:"periodsPerWeek"` PracticePeriods *float32 `json:"practicePeriods"` Remark *string `json:"remark"` + RetiredAt *time.Time `json:"retiredAt,omitempty"` RoomTypeId *int `json:"roomTypeId"` ScheduleJsonParams interface{} `json:"scheduleJsonParams"` ScheduleRemark *string `json:"scheduleRemark"` @@ -3163,6 +3340,7 @@ type CalendarSubscriptionImportResponseSchema struct { SelectedStdCount *int `json:"selectedStdCount"` Semester *SemesterSchema `json:"semester"` SemesterId *int `json:"semesterId"` + SourceLastSeenAt *time.Time `json:"sourceLastSeenAt,omitempty"` StdCount *int `json:"stdCount"` SuggestScheduleWeekInfo *string `json:"suggestScheduleWeekInfo"` SuggestScheduleWeeks interface{} `json:"suggestScheduleWeeks"` @@ -3285,6 +3463,7 @@ type CalendarSubscriptionImportResponseSchema struct { PeriodsPerWeek *int `json:"periodsPerWeek"` PracticePeriods *float32 `json:"practicePeriods"` Remark *string `json:"remark"` + RetiredAt *time.Time `json:"retiredAt,omitempty"` RoomTypeId *int `json:"roomTypeId"` ScheduleJsonParams interface{} `json:"scheduleJsonParams"` ScheduleRemark *string `json:"scheduleRemark"` @@ -3292,6 +3471,7 @@ type CalendarSubscriptionImportResponseSchema struct { SelectedStdCount *int `json:"selectedStdCount"` Semester *SemesterSchema `json:"semester"` SemesterId *int `json:"semesterId"` + SourceLastSeenAt *time.Time `json:"sourceLastSeenAt,omitempty"` StdCount *int `json:"stdCount"` SuggestScheduleWeekInfo *string `json:"suggestScheduleWeekInfo"` SuggestScheduleWeeks interface{} `json:"suggestScheduleWeeks"` @@ -3433,6 +3613,7 @@ type CalendarSubscriptionQueryResponseSchema struct { PeriodsPerWeek *int `json:"periodsPerWeek"` PracticePeriods *float32 `json:"practicePeriods"` Remark *string `json:"remark"` + RetiredAt *time.Time `json:"retiredAt,omitempty"` RoomTypeId *int `json:"roomTypeId"` ScheduleJsonParams interface{} `json:"scheduleJsonParams"` ScheduleRemark *string `json:"scheduleRemark"` @@ -3440,6 +3621,7 @@ type CalendarSubscriptionQueryResponseSchema struct { SelectedStdCount *int `json:"selectedStdCount"` Semester *SemesterSchema `json:"semester"` SemesterId *int `json:"semesterId"` + SourceLastSeenAt *time.Time `json:"sourceLastSeenAt,omitempty"` StdCount *int `json:"stdCount"` SuggestScheduleWeekInfo *string `json:"suggestScheduleWeekInfo"` SuggestScheduleWeeks interface{} `json:"suggestScheduleWeeks"` @@ -3575,6 +3757,7 @@ type CalendarSubscriptionRemoveResponseSchema struct { PeriodsPerWeek *int `json:"periodsPerWeek"` PracticePeriods *float32 `json:"practicePeriods"` Remark *string `json:"remark"` + RetiredAt *time.Time `json:"retiredAt,omitempty"` RoomTypeId *int `json:"roomTypeId"` ScheduleJsonParams interface{} `json:"scheduleJsonParams"` ScheduleRemark *string `json:"scheduleRemark"` @@ -3582,6 +3765,7 @@ type CalendarSubscriptionRemoveResponseSchema struct { SelectedStdCount *int `json:"selectedStdCount"` Semester *SemesterSchema `json:"semester"` SemesterId *int `json:"semesterId"` + SourceLastSeenAt *time.Time `json:"sourceLastSeenAt,omitempty"` StdCount *int `json:"stdCount"` SuggestScheduleWeekInfo *string `json:"suggestScheduleWeekInfo"` SuggestScheduleWeeks interface{} `json:"suggestScheduleWeeks"` @@ -3958,6 +4142,7 @@ type CompactOverviewResponseSchema struct { PeriodsPerWeek *int `json:"periodsPerWeek"` PracticePeriods *float32 `json:"practicePeriods"` Remark *string `json:"remark"` + RetiredAt *time.Time `json:"retiredAt,omitempty"` RoomTypeId *int `json:"roomTypeId"` ScheduleJsonParams interface{} `json:"scheduleJsonParams"` ScheduleRemark *string `json:"scheduleRemark"` @@ -3965,6 +4150,7 @@ type CompactOverviewResponseSchema struct { SelectedStdCount *int `json:"selectedStdCount"` Semester *SemesterSchema `json:"semester"` SemesterId *int `json:"semesterId"` + SourceLastSeenAt *time.Time `json:"sourceLastSeenAt,omitempty"` StdCount *int `json:"stdCount"` SuggestScheduleWeekInfo *string `json:"suggestScheduleWeekInfo"` SuggestScheduleWeeks interface{} `json:"suggestScheduleWeeks"` @@ -4053,6 +4239,7 @@ type CompactOverviewResponseSchema struct { PeriodsPerWeek *int `json:"periodsPerWeek"` PracticePeriods *float32 `json:"practicePeriods"` Remark *string `json:"remark"` + RetiredAt *time.Time `json:"retiredAt,omitempty"` RoomTypeId *int `json:"roomTypeId"` ScheduleJsonParams interface{} `json:"scheduleJsonParams"` ScheduleRemark *string `json:"scheduleRemark"` @@ -4060,6 +4247,7 @@ type CompactOverviewResponseSchema struct { SelectedStdCount *int `json:"selectedStdCount"` Semester *SemesterSchema `json:"semester"` SemesterId *int `json:"semesterId"` + SourceLastSeenAt *time.Time `json:"sourceLastSeenAt,omitempty"` StdCount *int `json:"stdCount"` SuggestScheduleWeekInfo *string `json:"suggestScheduleWeekInfo"` SuggestScheduleWeeks interface{} `json:"suggestScheduleWeeks"` @@ -4187,6 +4375,7 @@ type CompactOverviewResponseSchema struct { PeriodsPerWeek *int `json:"periodsPerWeek"` PracticePeriods *float32 `json:"practicePeriods"` Remark *string `json:"remark"` + RetiredAt *time.Time `json:"retiredAt,omitempty"` RoomTypeId *int `json:"roomTypeId"` ScheduleJsonParams interface{} `json:"scheduleJsonParams"` ScheduleRemark *string `json:"scheduleRemark"` @@ -4194,6 +4383,7 @@ type CompactOverviewResponseSchema struct { SelectedStdCount *int `json:"selectedStdCount"` Semester *SemesterSchema `json:"semester"` SemesterId *int `json:"semesterId"` + SourceLastSeenAt *time.Time `json:"sourceLastSeenAt,omitempty"` StdCount *int `json:"stdCount"` SuggestScheduleWeekInfo *string `json:"suggestScheduleWeekInfo"` SuggestScheduleWeeks interface{} `json:"suggestScheduleWeeks"` @@ -4336,6 +4526,7 @@ type CourseDetailSchema struct { PeriodsPerWeek *int `json:"periodsPerWeek"` PracticePeriods *float32 `json:"practicePeriods"` Remark *string `json:"remark"` + RetiredAt *time.Time `json:"retiredAt,omitempty"` RoomTypeId *int `json:"roomTypeId"` ScheduleJsonParams interface{} `json:"scheduleJsonParams"` ScheduleRemark *string `json:"scheduleRemark"` @@ -4343,6 +4534,7 @@ type CourseDetailSchema struct { SelectedStdCount *int `json:"selectedStdCount"` Semester *SemesterSchema `json:"semester"` SemesterId *int `json:"semesterId"` + SourceLastSeenAt *time.Time `json:"sourceLastSeenAt,omitempty"` StdCount *int `json:"stdCount"` SuggestScheduleWeekInfo *string `json:"suggestScheduleWeekInfo"` SuggestScheduleWeeks interface{} `json:"suggestScheduleWeeks"` @@ -4468,6 +4660,7 @@ type CurrentCalendarSubscriptionResponseSchema struct { PeriodsPerWeek *int `json:"periodsPerWeek"` PracticePeriods *float32 `json:"practicePeriods"` Remark *string `json:"remark"` + RetiredAt *time.Time `json:"retiredAt,omitempty"` RoomTypeId *int `json:"roomTypeId"` ScheduleJsonParams interface{} `json:"scheduleJsonParams"` ScheduleRemark *string `json:"scheduleRemark"` @@ -4475,6 +4668,7 @@ type CurrentCalendarSubscriptionResponseSchema struct { SelectedStdCount *int `json:"selectedStdCount"` Semester *SemesterSchema `json:"semester"` SemesterId *int `json:"semesterId"` + SourceLastSeenAt *time.Time `json:"sourceLastSeenAt,omitempty"` StdCount *int `json:"stdCount"` SuggestScheduleWeekInfo *string `json:"suggestScheduleWeekInfo"` SuggestScheduleWeeks interface{} `json:"suggestScheduleWeeks"` @@ -4652,7 +4846,8 @@ type DescriptionsResponseSchema struct { Name *string `json:"name"` Username *string `json:"username"` } `json:"lastEditedBy"` - UpdatedAt time.Time `json:"updatedAt"` + RenderedHtml string `json:"renderedHtml"` + UpdatedAt time.Time `json:"updatedAt"` } `json:"description"` History []struct { CreatedAt time.Time `json:"createdAt"` @@ -4952,6 +5147,7 @@ type HomeworkCreateResponseSchema struct { PeriodsPerWeek *int `json:"periodsPerWeek"` PracticePeriods *float32 `json:"practicePeriods"` Remark *string `json:"remark"` + RetiredAt *time.Time `json:"retiredAt,omitempty"` RoomTypeId *int `json:"roomTypeId"` ScheduleJsonParams interface{} `json:"scheduleJsonParams"` ScheduleRemark *string `json:"scheduleRemark"` @@ -4959,6 +5155,7 @@ type HomeworkCreateResponseSchema struct { SelectedStdCount *int `json:"selectedStdCount"` Semester *SemesterSchema `json:"semester"` SemesterId *int `json:"semesterId"` + SourceLastSeenAt *time.Time `json:"sourceLastSeenAt,omitempty"` StdCount *int `json:"stdCount"` SuggestScheduleWeekInfo *string `json:"suggestScheduleWeekInfo"` SuggestScheduleWeeks interface{} `json:"suggestScheduleWeeks"` @@ -5103,6 +5300,7 @@ type HomeworkUpdateResponseSchema struct { PeriodsPerWeek *int `json:"periodsPerWeek"` PracticePeriods *float32 `json:"practicePeriods"` Remark *string `json:"remark"` + RetiredAt *time.Time `json:"retiredAt,omitempty"` RoomTypeId *int `json:"roomTypeId"` ScheduleJsonParams interface{} `json:"scheduleJsonParams"` ScheduleRemark *string `json:"scheduleRemark"` @@ -5110,6 +5308,7 @@ type HomeworkUpdateResponseSchema struct { SelectedStdCount *int `json:"selectedStdCount"` Semester *SemesterSchema `json:"semester"` SemesterId *int `json:"semesterId"` + SourceLastSeenAt *time.Time `json:"sourceLastSeenAt,omitempty"` StdCount *int `json:"stdCount"` SuggestScheduleWeekInfo *string `json:"suggestScheduleWeekInfo"` SuggestScheduleWeeks interface{} `json:"suggestScheduleWeeks"` @@ -5225,6 +5424,7 @@ type HomeworksListResponseSchema struct { PeriodsPerWeek *int `json:"periodsPerWeek"` PracticePeriods *float32 `json:"practicePeriods"` Remark *string `json:"remark"` + RetiredAt *time.Time `json:"retiredAt,omitempty"` RoomTypeId *int `json:"roomTypeId"` ScheduleJsonParams interface{} `json:"scheduleJsonParams"` ScheduleRemark *string `json:"scheduleRemark"` @@ -5232,6 +5432,7 @@ type HomeworksListResponseSchema struct { SelectedStdCount *int `json:"selectedStdCount"` Semester *SemesterSchema `json:"semester"` SemesterId *int `json:"semesterId"` + SourceLastSeenAt *time.Time `json:"sourceLastSeenAt,omitempty"` StdCount *int `json:"stdCount"` SuggestScheduleWeekInfo *string `json:"suggestScheduleWeekInfo"` SuggestScheduleWeeks interface{} `json:"suggestScheduleWeeks"` @@ -5386,6 +5587,7 @@ type MatchSectionCodesResponseSchema struct { PeriodsPerWeek *int `json:"periodsPerWeek"` PracticePeriods *float32 `json:"practicePeriods"` Remark *string `json:"remark"` + RetiredAt *time.Time `json:"retiredAt,omitempty"` RoomTypeId *int `json:"roomTypeId"` ScheduleJsonParams interface{} `json:"scheduleJsonParams"` ScheduleRemark *string `json:"scheduleRemark"` @@ -5393,6 +5595,7 @@ type MatchSectionCodesResponseSchema struct { SelectedStdCount *int `json:"selectedStdCount"` Semester *SemesterSchema `json:"semester"` SemesterId *int `json:"semesterId"` + SourceLastSeenAt *time.Time `json:"sourceLastSeenAt,omitempty"` StdCount *int `json:"stdCount"` SuggestScheduleWeekInfo *string `json:"suggestScheduleWeekInfo"` SuggestScheduleWeeks interface{} `json:"suggestScheduleWeeks"` @@ -5753,6 +5956,7 @@ type PaginatedScheduleResponseSchema struct { PeriodsPerWeek *int `json:"periodsPerWeek"` PracticePeriods *float32 `json:"practicePeriods"` Remark *string `json:"remark"` + RetiredAt *time.Time `json:"retiredAt,omitempty"` RoomTypeId *int `json:"roomTypeId"` ScheduleJsonParams interface{} `json:"scheduleJsonParams"` ScheduleRemark *string `json:"scheduleRemark"` @@ -5760,6 +5964,7 @@ type PaginatedScheduleResponseSchema struct { SelectedStdCount *int `json:"selectedStdCount"` Semester *SemesterSchema `json:"semester"` SemesterId *int `json:"semesterId"` + SourceLastSeenAt *time.Time `json:"sourceLastSeenAt,omitempty"` StdCount *int `json:"stdCount"` SuggestScheduleWeekInfo *string `json:"suggestScheduleWeekInfo"` SuggestScheduleWeeks interface{} `json:"suggestScheduleWeeks"` @@ -6057,11 +6262,12 @@ type SectionDetailSchema struct { NameCn string `json:"nameCn"` NameEn *string `json:"nameEn"` } `json:"openDepartment"` - OpenDepartmentId *int `json:"openDepartmentId"` - Period *int `json:"period"` - PeriodsPerWeek *int `json:"periodsPerWeek"` - PracticePeriods *float32 `json:"practicePeriods"` - Remark *string `json:"remark"` + OpenDepartmentId *int `json:"openDepartmentId"` + Period *int `json:"period"` + PeriodsPerWeek *int `json:"periodsPerWeek"` + PracticePeriods *float32 `json:"practicePeriods"` + Remark *string `json:"remark"` + RetiredAt *time.Time `json:"retiredAt,omitempty"` RoomType *struct { Code string `json:"code"` Id int `json:"id"` @@ -6104,6 +6310,7 @@ type SectionDetailSchema struct { SelectedStdCount *int `json:"selectedStdCount"` Semester *SemesterSchema `json:"semester"` SemesterId *int `json:"semesterId"` + SourceLastSeenAt *time.Time `json:"sourceLastSeenAt,omitempty"` StdCount *int `json:"stdCount"` SuggestScheduleWeekInfo *string `json:"suggestScheduleWeekInfo"` SuggestScheduleWeeks interface{} `json:"suggestScheduleWeeks"` @@ -6289,6 +6496,7 @@ type SubscribedHomeworksResponseSchema struct { PeriodsPerWeek *int `json:"periodsPerWeek"` PracticePeriods *float32 `json:"practicePeriods"` Remark *string `json:"remark"` + RetiredAt *time.Time `json:"retiredAt,omitempty"` RoomTypeId *int `json:"roomTypeId"` ScheduleJsonParams interface{} `json:"scheduleJsonParams"` ScheduleRemark *string `json:"scheduleRemark"` @@ -6296,6 +6504,7 @@ type SubscribedHomeworksResponseSchema struct { SelectedStdCount *int `json:"selectedStdCount"` Semester *SemesterSchema `json:"semester"` SemesterId *int `json:"semesterId"` + SourceLastSeenAt *time.Time `json:"sourceLastSeenAt,omitempty"` StdCount *int `json:"stdCount"` SuggestScheduleWeekInfo *string `json:"suggestScheduleWeekInfo"` SuggestScheduleWeeks interface{} `json:"suggestScheduleWeeks"` @@ -6438,6 +6647,7 @@ type SubscribedSchedulesResponseSchema struct { PeriodsPerWeek *int `json:"periodsPerWeek"` PracticePeriods *float32 `json:"practicePeriods"` Remark *string `json:"remark"` + RetiredAt *time.Time `json:"retiredAt,omitempty"` RoomTypeId *int `json:"roomTypeId"` ScheduleJsonParams interface{} `json:"scheduleJsonParams"` ScheduleRemark *string `json:"scheduleRemark"` @@ -6445,6 +6655,7 @@ type SubscribedSchedulesResponseSchema struct { SelectedStdCount *int `json:"selectedStdCount"` Semester *SemesterSchema `json:"semester"` SemesterId *int `json:"semesterId"` + SourceLastSeenAt *time.Time `json:"sourceLastSeenAt,omitempty"` StdCount *int `json:"stdCount"` SuggestScheduleWeekInfo *string `json:"suggestScheduleWeekInfo"` SuggestScheduleWeeks interface{} `json:"suggestScheduleWeeks"` @@ -6584,6 +6795,7 @@ type TeacherDetailSchema struct { PeriodsPerWeek *int `json:"periodsPerWeek"` PracticePeriods *float32 `json:"practicePeriods"` Remark *string `json:"remark"` + RetiredAt *time.Time `json:"retiredAt,omitempty"` RoomTypeId *int `json:"roomTypeId"` ScheduleJsonParams interface{} `json:"scheduleJsonParams"` ScheduleRemark *string `json:"scheduleRemark"` @@ -6591,6 +6803,7 @@ type TeacherDetailSchema struct { SelectedStdCount *int `json:"selectedStdCount"` Semester *SemesterSchema `json:"semester"` SemesterId *int `json:"semesterId"` + SourceLastSeenAt *time.Time `json:"sourceLastSeenAt,omitempty"` StdCount *int `json:"stdCount"` SuggestScheduleWeekInfo *string `json:"suggestScheduleWeekInfo"` SuggestScheduleWeeks interface{} `json:"suggestScheduleWeeks"` @@ -7019,11 +7232,12 @@ type RemoveCommentReactionParamsType string // ListCoursesParams defines parameters for ListCourses. type ListCoursesParams struct { - Search *string `form:"search,omitempty" json:"search,omitempty"` - EducationLevelId *int64 `form:"educationLevelId,omitempty" json:"educationLevelId,omitempty"` - CategoryId *int64 `form:"categoryId,omitempty" json:"categoryId,omitempty"` - ClassTypeId *int64 `form:"classTypeId,omitempty" json:"classTypeId,omitempty"` - Page *int64 `form:"page,omitempty" json:"page,omitempty"` + Locale *ListCoursesParamsLocale `form:"locale,omitempty" json:"locale,omitempty"` + Search *string `form:"search,omitempty" json:"search,omitempty"` + EducationLevelId *int64 `form:"educationLevelId,omitempty" json:"educationLevelId,omitempty"` + CategoryId *int64 `form:"categoryId,omitempty" json:"categoryId,omitempty"` + ClassTypeId *int64 `form:"classTypeId,omitempty" json:"classTypeId,omitempty"` + Page *int64 `form:"page,omitempty" json:"page,omitempty"` // PageSize Number of items per page. PageSize *int64 `form:"pageSize,omitempty" json:"pageSize,omitempty"` @@ -7032,6 +7246,17 @@ type ListCoursesParams struct { Limit *int64 `form:"limit,omitempty" json:"limit,omitempty"` } +// ListCoursesParamsLocale defines parameters for ListCourses. +type ListCoursesParamsLocale string + +// GetCourseParams defines parameters for GetCourse. +type GetCourseParams struct { + Locale *GetCourseParamsLocale `form:"locale,omitempty" json:"locale,omitempty"` +} + +// GetCourseParamsLocale defines parameters for GetCourse. +type GetCourseParamsLocale string + // VisitDashboardLinkParams defines parameters for VisitDashboardLink. type VisitDashboardLinkParams struct { Slug string `form:"slug" json:"slug"` @@ -7086,17 +7311,18 @@ type GetApiMeSubscriptionsSchedulesParamsLocale string // ListSchedulesParams defines parameters for ListSchedules. type ListSchedulesParams struct { - SectionId *int64 `form:"sectionId,omitempty" json:"sectionId,omitempty"` - SectionJwId *int64 `form:"sectionJwId,omitempty" json:"sectionJwId,omitempty"` - SectionCode *string `form:"sectionCode,omitempty" json:"sectionCode,omitempty"` - TeacherId *int64 `form:"teacherId,omitempty" json:"teacherId,omitempty"` - TeacherCode *string `form:"teacherCode,omitempty" json:"teacherCode,omitempty"` - RoomId *int64 `form:"roomId,omitempty" json:"roomId,omitempty"` - RoomJwId *int64 `form:"roomJwId,omitempty" json:"roomJwId,omitempty"` - Weekday *int64 `form:"weekday,omitempty" json:"weekday,omitempty"` - DateFrom *string `form:"dateFrom,omitempty" json:"dateFrom,omitempty"` - DateTo *string `form:"dateTo,omitempty" json:"dateTo,omitempty"` - Page *int64 `form:"page,omitempty" json:"page,omitempty"` + Locale *ListSchedulesParamsLocale `form:"locale,omitempty" json:"locale,omitempty"` + SectionId *int64 `form:"sectionId,omitempty" json:"sectionId,omitempty"` + SectionJwId *int64 `form:"sectionJwId,omitempty" json:"sectionJwId,omitempty"` + SectionCode *string `form:"sectionCode,omitempty" json:"sectionCode,omitempty"` + TeacherId *int64 `form:"teacherId,omitempty" json:"teacherId,omitempty"` + TeacherCode *string `form:"teacherCode,omitempty" json:"teacherCode,omitempty"` + RoomId *int64 `form:"roomId,omitempty" json:"roomId,omitempty"` + RoomJwId *int64 `form:"roomJwId,omitempty" json:"roomJwId,omitempty"` + Weekday *int64 `form:"weekday,omitempty" json:"weekday,omitempty"` + DateFrom *string `form:"dateFrom,omitempty" json:"dateFrom,omitempty"` + DateTo *string `form:"dateTo,omitempty" json:"dateTo,omitempty"` + Page *int64 `form:"page,omitempty" json:"page,omitempty"` // PageSize Number of items per page. PageSize *int64 `form:"pageSize,omitempty" json:"pageSize,omitempty"` @@ -7105,20 +7331,24 @@ type ListSchedulesParams struct { Limit *int64 `form:"limit,omitempty" json:"limit,omitempty"` } +// ListSchedulesParamsLocale defines parameters for ListSchedules. +type ListSchedulesParamsLocale string + // ListSectionsParams defines parameters for ListSections. type ListSectionsParams struct { - CourseId *int64 `form:"courseId,omitempty" json:"courseId,omitempty"` - CourseJwId *int64 `form:"courseJwId,omitempty" json:"courseJwId,omitempty"` - SemesterId *int64 `form:"semesterId,omitempty" json:"semesterId,omitempty"` - SemesterJwId *int64 `form:"semesterJwId,omitempty" json:"semesterJwId,omitempty"` - CampusId *int64 `form:"campusId,omitempty" json:"campusId,omitempty"` - DepartmentId *int64 `form:"departmentId,omitempty" json:"departmentId,omitempty"` - TeacherId *int64 `form:"teacherId,omitempty" json:"teacherId,omitempty"` - TeacherCode *string `form:"teacherCode,omitempty" json:"teacherCode,omitempty"` - Search *string `form:"search,omitempty" json:"search,omitempty"` - Ids *string `form:"ids,omitempty" json:"ids,omitempty"` - JwIds *string `form:"jwIds,omitempty" json:"jwIds,omitempty"` - Page *int64 `form:"page,omitempty" json:"page,omitempty"` + Locale *ListSectionsParamsLocale `form:"locale,omitempty" json:"locale,omitempty"` + CourseId *int64 `form:"courseId,omitempty" json:"courseId,omitempty"` + CourseJwId *int64 `form:"courseJwId,omitempty" json:"courseJwId,omitempty"` + SemesterId *int64 `form:"semesterId,omitempty" json:"semesterId,omitempty"` + SemesterJwId *int64 `form:"semesterJwId,omitempty" json:"semesterJwId,omitempty"` + CampusId *int64 `form:"campusId,omitempty" json:"campusId,omitempty"` + DepartmentId *int64 `form:"departmentId,omitempty" json:"departmentId,omitempty"` + TeacherId *int64 `form:"teacherId,omitempty" json:"teacherId,omitempty"` + TeacherCode *string `form:"teacherCode,omitempty" json:"teacherCode,omitempty"` + Search *string `form:"search,omitempty" json:"search,omitempty"` + Ids *string `form:"ids,omitempty" json:"ids,omitempty"` + JwIds *string `form:"jwIds,omitempty" json:"jwIds,omitempty"` + Page *int64 `form:"page,omitempty" json:"page,omitempty"` // PageSize Number of items per page. PageSize *int64 `form:"pageSize,omitempty" json:"pageSize,omitempty"` @@ -7127,18 +7357,41 @@ type ListSectionsParams struct { Limit *int64 `form:"limit,omitempty" json:"limit,omitempty"` } +// ListSectionsParamsLocale defines parameters for ListSections. +type ListSectionsParamsLocale string + // GetSectionsCalendarParams defines parameters for GetSectionsCalendar. type GetSectionsCalendarParams struct { SectionIds string `form:"sectionIds" json:"sectionIds"` } +// GetSectionParams defines parameters for GetSection. +type GetSectionParams struct { + Locale *GetSectionParamsLocale `form:"locale,omitempty" json:"locale,omitempty"` +} + +// GetSectionParamsLocale defines parameters for GetSection. +type GetSectionParamsLocale string + +// GetSectionScheduleGroupsParams defines parameters for GetSectionScheduleGroups. +type GetSectionScheduleGroupsParams struct { + Locale *GetSectionScheduleGroupsParamsLocale `form:"locale,omitempty" json:"locale,omitempty"` +} + +// GetSectionScheduleGroupsParamsLocale defines parameters for GetSectionScheduleGroups. +type GetSectionScheduleGroupsParamsLocale string + // GetSectionSchedulesParams defines parameters for GetSectionSchedules. type GetSectionSchedulesParams struct { - DateFrom *string `form:"dateFrom,omitempty" json:"dateFrom,omitempty"` - DateTo *string `form:"dateTo,omitempty" json:"dateTo,omitempty"` - Limit *int64 `form:"limit,omitempty" json:"limit,omitempty"` + Locale *GetSectionSchedulesParamsLocale `form:"locale,omitempty" json:"locale,omitempty"` + DateFrom *string `form:"dateFrom,omitempty" json:"dateFrom,omitempty"` + DateTo *string `form:"dateTo,omitempty" json:"dateTo,omitempty"` + Limit *int64 `form:"limit,omitempty" json:"limit,omitempty"` } +// GetSectionSchedulesParamsLocale defines parameters for GetSectionSchedules. +type GetSectionSchedulesParamsLocale string + // ListSemestersParams defines parameters for ListSemesters. type ListSemestersParams struct { Page *int64 `form:"page,omitempty" json:"page,omitempty"` @@ -7152,9 +7405,10 @@ type ListSemestersParams struct { // ListTeachersParams defines parameters for ListTeachers. type ListTeachersParams struct { - DepartmentId *int64 `form:"departmentId,omitempty" json:"departmentId,omitempty"` - Search *string `form:"search,omitempty" json:"search,omitempty"` - Page *int64 `form:"page,omitempty" json:"page,omitempty"` + Locale *ListTeachersParamsLocale `form:"locale,omitempty" json:"locale,omitempty"` + DepartmentId *int64 `form:"departmentId,omitempty" json:"departmentId,omitempty"` + Search *string `form:"search,omitempty" json:"search,omitempty"` + Page *int64 `form:"page,omitempty" json:"page,omitempty"` // PageSize Number of items per page. PageSize *int64 `form:"pageSize,omitempty" json:"pageSize,omitempty"` @@ -7163,6 +7417,17 @@ type ListTeachersParams struct { Limit *int64 `form:"limit,omitempty" json:"limit,omitempty"` } +// ListTeachersParamsLocale defines parameters for ListTeachers. +type ListTeachersParamsLocale string + +// GetTeacherParams defines parameters for GetTeacher. +type GetTeacherParams struct { + Locale *GetTeacherParamsLocale `form:"locale,omitempty" json:"locale,omitempty"` +} + +// GetTeacherParamsLocale defines parameters for GetTeacher. +type GetTeacherParamsLocale string + // ListTodosParams defines parameters for ListTodos. type ListTodosParams struct { Completed *ListTodosParamsCompleted `form:"completed,omitempty" json:"completed,omitempty"` @@ -10256,7 +10521,7 @@ type ClientInterface interface { ListCourses(ctx context.Context, params *ListCoursesParams, reqEditors ...RequestEditorFn) (*http.Response, error) // GetCourse request - GetCourse(ctx context.Context, jwId int64, reqEditors ...RequestEditorFn) (*http.Response, error) + GetCourse(ctx context.Context, jwId int64, params *GetCourseParams, reqEditors ...RequestEditorFn) (*http.Response, error) // PinDashboardLinkWithBody request with any body PinDashboardLinkWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -10348,13 +10613,13 @@ type ClientInterface interface { MatchSectionCodes(ctx context.Context, body MatchSectionCodesJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) // GetSection request - GetSection(ctx context.Context, jwId int64, reqEditors ...RequestEditorFn) (*http.Response, error) + GetSection(ctx context.Context, jwId int64, params *GetSectionParams, reqEditors ...RequestEditorFn) (*http.Response, error) // GetSectionCalendar request GetSectionCalendar(ctx context.Context, jwId int64, reqEditors ...RequestEditorFn) (*http.Response, error) // GetSectionScheduleGroups request - GetSectionScheduleGroups(ctx context.Context, jwId int64, reqEditors ...RequestEditorFn) (*http.Response, error) + GetSectionScheduleGroups(ctx context.Context, jwId int64, params *GetSectionScheduleGroupsParams, reqEditors ...RequestEditorFn) (*http.Response, error) // GetSectionSchedules request GetSectionSchedules(ctx context.Context, jwId int64, params *GetSectionSchedulesParams, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -10369,7 +10634,7 @@ type ClientInterface interface { ListTeachers(ctx context.Context, params *ListTeachersParams, reqEditors ...RequestEditorFn) (*http.Response, error) // GetTeacher request - GetTeacher(ctx context.Context, id int64, reqEditors ...RequestEditorFn) (*http.Response, error) + GetTeacher(ctx context.Context, id int64, params *GetTeacherParams, reqEditors ...RequestEditorFn) (*http.Response, error) // ListTodos request ListTodos(ctx context.Context, params *ListTodosParams, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -10992,8 +11257,8 @@ func (c *Client) ListCourses(ctx context.Context, params *ListCoursesParams, req return c.Client.Do(req) } -func (c *Client) GetCourse(ctx context.Context, jwId int64, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewGetCourseRequest(c.Server, jwId) +func (c *Client) GetCourse(ctx context.Context, jwId int64, params *GetCourseParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetCourseRequest(c.Server, jwId, params) if err != nil { return nil, err } @@ -11400,8 +11665,8 @@ func (c *Client) MatchSectionCodes(ctx context.Context, body MatchSectionCodesJS return c.Client.Do(req) } -func (c *Client) GetSection(ctx context.Context, jwId int64, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewGetSectionRequest(c.Server, jwId) +func (c *Client) GetSection(ctx context.Context, jwId int64, params *GetSectionParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetSectionRequest(c.Server, jwId, params) if err != nil { return nil, err } @@ -11424,8 +11689,8 @@ func (c *Client) GetSectionCalendar(ctx context.Context, jwId int64, reqEditors return c.Client.Do(req) } -func (c *Client) GetSectionScheduleGroups(ctx context.Context, jwId int64, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewGetSectionScheduleGroupsRequest(c.Server, jwId) +func (c *Client) GetSectionScheduleGroups(ctx context.Context, jwId int64, params *GetSectionScheduleGroupsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetSectionScheduleGroupsRequest(c.Server, jwId, params) if err != nil { return nil, err } @@ -11484,8 +11749,8 @@ func (c *Client) ListTeachers(ctx context.Context, params *ListTeachersParams, r return c.Client.Do(req) } -func (c *Client) GetTeacher(ctx context.Context, id int64, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewGetTeacherRequest(c.Server, id) +func (c *Client) GetTeacher(ctx context.Context, id int64, params *GetTeacherParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetTeacherRequest(c.Server, id, params) if err != nil { return nil, err } @@ -13614,6 +13879,22 @@ func NewListCoursesRequest(server string, params *ListCoursesParams) (*http.Requ if params != nil { queryValues := queryURL.Query() + if params.Locale != nil { + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "locale", *params.Locale, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + if params.Search != nil { if queryFrag, err := runtime.StyleParamWithOptions("form", true, "search", *params.Search, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { @@ -13738,7 +14019,7 @@ func NewListCoursesRequest(server string, params *ListCoursesParams) (*http.Requ } // NewGetCourseRequest generates requests for GetCourse -func NewGetCourseRequest(server string, jwId int64) (*http.Request, error) { +func NewGetCourseRequest(server string, jwId int64, params *GetCourseParams) (*http.Request, error) { var err error var pathParam0 string @@ -13763,6 +14044,28 @@ func NewGetCourseRequest(server string, jwId int64) (*http.Request, error) { return nil, err } + if params != nil { + queryValues := queryURL.Query() + + if params.Locale != nil { + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "locale", *params.Locale, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + queryURL.RawQuery = queryValues.Encode() + } + req, err := http.NewRequest("GET", queryURL.String(), nil) if err != nil { return nil, err @@ -14786,6 +15089,22 @@ func NewListSchedulesRequest(server string, params *ListSchedulesParams) (*http. if params != nil { queryValues := queryURL.Query() + if params.Locale != nil { + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "locale", *params.Locale, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + if params.SectionId != nil { if queryFrag, err := runtime.StyleParamWithOptions("form", true, "sectionId", *params.SectionId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "integer", Format: "int64"}); err != nil { @@ -15027,6 +15346,22 @@ func NewListSectionsRequest(server string, params *ListSectionsParams) (*http.Re if params != nil { queryValues := queryURL.Query() + if params.Locale != nil { + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "locale", *params.Locale, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + if params.CourseId != nil { if queryFrag, err := runtime.StyleParamWithOptions("form", true, "courseId", *params.CourseId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "integer", Format: "int64"}); err != nil { @@ -15348,7 +15683,7 @@ func NewMatchSectionCodesRequestWithBody(server string, contentType string, body } // NewGetSectionRequest generates requests for GetSection -func NewGetSectionRequest(server string, jwId int64) (*http.Request, error) { +func NewGetSectionRequest(server string, jwId int64, params *GetSectionParams) (*http.Request, error) { var err error var pathParam0 string @@ -15373,6 +15708,28 @@ func NewGetSectionRequest(server string, jwId int64) (*http.Request, error) { return nil, err } + if params != nil { + queryValues := queryURL.Query() + + if params.Locale != nil { + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "locale", *params.Locale, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + queryURL.RawQuery = queryValues.Encode() + } + req, err := http.NewRequest("GET", queryURL.String(), nil) if err != nil { return nil, err @@ -15416,7 +15773,7 @@ func NewGetSectionCalendarRequest(server string, jwId int64) (*http.Request, err } // NewGetSectionScheduleGroupsRequest generates requests for GetSectionScheduleGroups -func NewGetSectionScheduleGroupsRequest(server string, jwId int64) (*http.Request, error) { +func NewGetSectionScheduleGroupsRequest(server string, jwId int64, params *GetSectionScheduleGroupsParams) (*http.Request, error) { var err error var pathParam0 string @@ -15441,6 +15798,28 @@ func NewGetSectionScheduleGroupsRequest(server string, jwId int64) (*http.Reques return nil, err } + if params != nil { + queryValues := queryURL.Query() + + if params.Locale != nil { + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "locale", *params.Locale, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + queryURL.RawQuery = queryValues.Encode() + } + req, err := http.NewRequest("GET", queryURL.String(), nil) if err != nil { return nil, err @@ -15478,6 +15857,22 @@ func NewGetSectionSchedulesRequest(server string, jwId int64, params *GetSection if params != nil { queryValues := queryURL.Query() + if params.Locale != nil { + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "locale", *params.Locale, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + if params.DateFrom != nil { if queryFrag, err := runtime.StyleParamWithOptions("form", true, "dateFrom", *params.DateFrom, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { @@ -15667,6 +16062,22 @@ func NewListTeachersRequest(server string, params *ListTeachersParams) (*http.Re if params != nil { queryValues := queryURL.Query() + if params.Locale != nil { + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "locale", *params.Locale, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + if params.DepartmentId != nil { if queryFrag, err := runtime.StyleParamWithOptions("form", true, "departmentId", *params.DepartmentId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "integer", Format: "int64"}); err != nil { @@ -15759,7 +16170,7 @@ func NewListTeachersRequest(server string, params *ListTeachersParams) (*http.Re } // NewGetTeacherRequest generates requests for GetTeacher -func NewGetTeacherRequest(server string, id int64) (*http.Request, error) { +func NewGetTeacherRequest(server string, id int64, params *GetTeacherParams) (*http.Request, error) { var err error var pathParam0 string @@ -15784,6 +16195,28 @@ func NewGetTeacherRequest(server string, id int64) (*http.Request, error) { return nil, err } + if params != nil { + queryValues := queryURL.Query() + + if params.Locale != nil { + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "locale", *params.Locale, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + queryURL.RawQuery = queryValues.Encode() + } + req, err := http.NewRequest("GET", queryURL.String(), nil) if err != nil { return nil, err @@ -16655,7 +17088,7 @@ type ClientWithResponsesInterface interface { ListCoursesWithResponse(ctx context.Context, params *ListCoursesParams, reqEditors ...RequestEditorFn) (*ListCoursesResponse, error) // GetCourseWithResponse request - GetCourseWithResponse(ctx context.Context, jwId int64, reqEditors ...RequestEditorFn) (*GetCourseResponse, error) + GetCourseWithResponse(ctx context.Context, jwId int64, params *GetCourseParams, reqEditors ...RequestEditorFn) (*GetCourseResponse, error) // PinDashboardLinkWithBodyWithResponse request with any body PinDashboardLinkWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PinDashboardLinkResponse, error) @@ -16747,13 +17180,13 @@ type ClientWithResponsesInterface interface { MatchSectionCodesWithResponse(ctx context.Context, body MatchSectionCodesJSONRequestBody, reqEditors ...RequestEditorFn) (*MatchSectionCodesResponse, error) // GetSectionWithResponse request - GetSectionWithResponse(ctx context.Context, jwId int64, reqEditors ...RequestEditorFn) (*GetSectionResponse, error) + GetSectionWithResponse(ctx context.Context, jwId int64, params *GetSectionParams, reqEditors ...RequestEditorFn) (*GetSectionResponse, error) // GetSectionCalendarWithResponse request GetSectionCalendarWithResponse(ctx context.Context, jwId int64, reqEditors ...RequestEditorFn) (*GetSectionCalendarResponse, error) // GetSectionScheduleGroupsWithResponse request - GetSectionScheduleGroupsWithResponse(ctx context.Context, jwId int64, reqEditors ...RequestEditorFn) (*GetSectionScheduleGroupsResponse, error) + GetSectionScheduleGroupsWithResponse(ctx context.Context, jwId int64, params *GetSectionScheduleGroupsParams, reqEditors ...RequestEditorFn) (*GetSectionScheduleGroupsResponse, error) // GetSectionSchedulesWithResponse request GetSectionSchedulesWithResponse(ctx context.Context, jwId int64, params *GetSectionSchedulesParams, reqEditors ...RequestEditorFn) (*GetSectionSchedulesResponse, error) @@ -16768,7 +17201,7 @@ type ClientWithResponsesInterface interface { ListTeachersWithResponse(ctx context.Context, params *ListTeachersParams, reqEditors ...RequestEditorFn) (*ListTeachersResponse, error) // GetTeacherWithResponse request - GetTeacherWithResponse(ctx context.Context, id int64, reqEditors ...RequestEditorFn) (*GetTeacherResponse, error) + GetTeacherWithResponse(ctx context.Context, id int64, params *GetTeacherParams, reqEditors ...RequestEditorFn) (*GetTeacherResponse, error) // ListTodosWithResponse request ListTodosWithResponse(ctx context.Context, params *ListTodosParams, reqEditors ...RequestEditorFn) (*ListTodosResponse, error) @@ -19197,8 +19630,8 @@ func (c *ClientWithResponses) ListCoursesWithResponse(ctx context.Context, param } // GetCourseWithResponse request returning *GetCourseResponse -func (c *ClientWithResponses) GetCourseWithResponse(ctx context.Context, jwId int64, reqEditors ...RequestEditorFn) (*GetCourseResponse, error) { - rsp, err := c.GetCourse(ctx, jwId, reqEditors...) +func (c *ClientWithResponses) GetCourseWithResponse(ctx context.Context, jwId int64, params *GetCourseParams, reqEditors ...RequestEditorFn) (*GetCourseResponse, error) { + rsp, err := c.GetCourse(ctx, jwId, params, reqEditors...) if err != nil { return nil, err } @@ -19493,8 +19926,8 @@ func (c *ClientWithResponses) MatchSectionCodesWithResponse(ctx context.Context, } // GetSectionWithResponse request returning *GetSectionResponse -func (c *ClientWithResponses) GetSectionWithResponse(ctx context.Context, jwId int64, reqEditors ...RequestEditorFn) (*GetSectionResponse, error) { - rsp, err := c.GetSection(ctx, jwId, reqEditors...) +func (c *ClientWithResponses) GetSectionWithResponse(ctx context.Context, jwId int64, params *GetSectionParams, reqEditors ...RequestEditorFn) (*GetSectionResponse, error) { + rsp, err := c.GetSection(ctx, jwId, params, reqEditors...) if err != nil { return nil, err } @@ -19511,8 +19944,8 @@ func (c *ClientWithResponses) GetSectionCalendarWithResponse(ctx context.Context } // GetSectionScheduleGroupsWithResponse request returning *GetSectionScheduleGroupsResponse -func (c *ClientWithResponses) GetSectionScheduleGroupsWithResponse(ctx context.Context, jwId int64, reqEditors ...RequestEditorFn) (*GetSectionScheduleGroupsResponse, error) { - rsp, err := c.GetSectionScheduleGroups(ctx, jwId, reqEditors...) +func (c *ClientWithResponses) GetSectionScheduleGroupsWithResponse(ctx context.Context, jwId int64, params *GetSectionScheduleGroupsParams, reqEditors ...RequestEditorFn) (*GetSectionScheduleGroupsResponse, error) { + rsp, err := c.GetSectionScheduleGroups(ctx, jwId, params, reqEditors...) if err != nil { return nil, err } @@ -19556,8 +19989,8 @@ func (c *ClientWithResponses) ListTeachersWithResponse(ctx context.Context, para } // GetTeacherWithResponse request returning *GetTeacherResponse -func (c *ClientWithResponses) GetTeacherWithResponse(ctx context.Context, id int64, reqEditors ...RequestEditorFn) (*GetTeacherResponse, error) { - rsp, err := c.GetTeacher(ctx, id, reqEditors...) +func (c *ClientWithResponses) GetTeacherWithResponse(ctx context.Context, id int64, params *GetTeacherParams, reqEditors ...RequestEditorFn) (*GetTeacherResponse, error) { + rsp, err := c.GetTeacher(ctx, id, params, reqEditors...) if err != nil { return nil, err } diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 4eaac05..f93fcac 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -9,7 +9,9 @@ import ( "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "github.com/fatih/color" "github.com/mattn/go-runewidth" + "github.com/muesli/termenv" ) type SearchForm struct { @@ -36,6 +38,7 @@ type SearchTable struct { Form SearchForm Columns []output.Column Fetch func(SearchResult) (TableResult, error) + OnSelect func(map[string]any) error EmptyMessage string } @@ -48,17 +51,18 @@ const ( ) type tableModel struct { - spec SearchTable - inputs []textinput.Model - focus int - err string - mode tableMode - query SearchResult - result TableResult - selected int - scroll int - width int - height int + spec SearchTable + inputs []textinput.Model + focus int + err string + mode tableMode + query SearchResult + result TableResult + selected int + selection map[string]any + scroll int + width int + height int } type tableFetchMsg struct { @@ -70,9 +74,29 @@ func RunSearchTable(spec SearchTable) error { if spec.Fetch == nil { return fmt.Errorf("missing TUI fetch function") } + restoreColorProfile := configureSearchTableColors() + defer restoreColorProfile() + model := newTableModel(spec) - _, err := tea.NewProgram(model, tea.WithAltScreen()).Run() - return err + finalModel, err := tea.NewProgram(model, tea.WithAltScreen()).Run() + if err != nil { + return err + } + if result, ok := finalModel.(tableModel); ok && result.selection != nil && spec.OnSelect != nil { + return spec.OnSelect(result.selection) + } + return nil +} + +func configureSearchTableColors() func() { + if !output.Current.NoColor && !color.NoColor { + return func() {} + } + previous := lipgloss.ColorProfile() + lipgloss.SetColorProfile(termenv.Ascii) + return func() { + lipgloss.SetColorProfile(previous) + } } func newTableModel(spec SearchTable) tableModel { @@ -191,7 +215,16 @@ func (m tableModel) updateResults(msg tea.KeyMsg) (tea.Model, tea.Cmd) { switch msg.String() { case "ctrl+c", "esc", "q": return m, tea.Quit - case "/", "e", "enter": + case "/", "e": + m.mode = tableModeSearch + m.err = "" + m = m.focusInput(0) + return m, nil + case "enter": + if m.spec.OnSelect != nil && len(m.result.Rows) > 0 { + m.selection = m.result.Rows[m.selected] + return m, tea.Quit + } m.mode = tableModeSearch m.err = "" m = m.focusInput(0) @@ -288,7 +321,7 @@ func (m tableModel) searchView() string { } body := lipgloss.JoinVertical(lipgloss.Left, parts...) - return formBoxStyle.Width(contentWidth).Render(body) + return renderFormBox(body, contentWidth) } func (m tableModel) loadingView() string { @@ -302,7 +335,7 @@ func (m tableModel) loadingView() string { "", mutedStyle.Render("esc quit"), ) - return formBoxStyle.Width(contentWidth).Render(body) + return renderFormBox(body, contentWidth) } func (m tableModel) resultsView() string { @@ -324,10 +357,10 @@ func (m tableModel) resultsView() string { mutedStyle.Width(contentWidth).Render(helpText), ) body := lipgloss.JoinVertical(lipgloss.Left, parts...) - return formBoxStyle.Width(contentWidth).Render(body) + return renderFormBox(body, contentWidth) } - parts = append(parts, mutedStyle.Render(resultSummary(m.result))) + parts = append(parts, mutedStyle.Width(contentWidth).Render(resultSummary(m.result))) if len(m.result.Rows) == 0 { msg := m.spec.EmptyMessage if msg == "" { @@ -350,7 +383,15 @@ func (m tableModel) resultsView() string { parts = append(parts, "", mutedStyle.Width(contentWidth).Render(helpText)) } body := lipgloss.JoinVertical(lipgloss.Left, parts...) - return formBoxStyle.Width(contentWidth).Render(body) + return renderFormBox(body, contentWidth) +} + +func renderFormBox(body string, width int) string { + lines := strings.Split(body, "\n") + for i, line := range lines { + lines[i] = line + strings.Repeat(" ", max(0, width-lipgloss.Width(line))) + } + return formBoxStyle.Render(strings.Join(lines, "\n")) } func (m tableModel) renderTable(width int) string { @@ -366,7 +407,7 @@ func (m tableModel) renderTable(width int) string { absolute := m.scroll + i line := tableLine(cols, row, colWidths, gap, width, false) if absolute == m.selected { - line = selectedRowStyle.Width(width).Render(line) + line = selectedRowStyle.Render(line) } lines = append(lines, line) } @@ -427,20 +468,40 @@ func (m tableModel) canPageNext() bool { func (m tableModel) resultsHelpText(compact bool) string { if compact { + if m.spec.OnSelect != nil && len(m.result.Rows) > 0 { + if m.canPageNext() || m.currentPage() > 1 { + return "enter open • p/n • / edit • esc" + } + return "enter open • ↑/↓ • / edit • esc" + } if m.canPageNext() || m.currentPage() > 1 { return "p/n page • / edit • esc" } return "↑/↓ • r • / edit • esc" } - parts := []string{"↑/↓ move", "r refresh"} - if m.currentPage() > 1 { + if m.spec.OnSelect == nil || len(m.result.Rows) == 0 { + parts := []string{"↑/↓ move", "r refresh"} + if m.currentPage() > 1 { + parts = append(parts, "p prev") + } + if m.canPageNext() { + parts = append(parts, "n next") + } + parts = append(parts, "/ edit search", "esc quit") + return strings.Join(parts, " • ") + } + parts := make([]string, 0, 6) + parts = append(parts, "enter open") + parts = append(parts, "↑/↓ move", "r refresh") + if m.currentPage() > 1 && m.canPageNext() { + parts = append(parts, "p/n page") + } else if m.currentPage() > 1 { parts = append(parts, "p prev") - } - if m.canPageNext() { + } else if m.canPageNext() { parts = append(parts, "n next") } - parts = append(parts, "/ edit search", "esc quit") - return strings.Join(parts, " • ") + parts = append(parts, "/ edit", "esc quit") + return strings.Join(parts, " • ") } func (m tableModel) focusNext() tableModel { @@ -477,8 +538,6 @@ func (m tableModel) resultFromInputs() (SearchResult, error) { }, nil } - - func (m tableModel) searchContentWidth() int { return contentWidth(m.width, maxFormOuterWidth) } @@ -615,7 +674,6 @@ func tableCell(row map[string]any, col output.Column) string { return output.FormatCellPlain(output.Resolve(row, col.Key)) } - func padCell(value string, width int) string { if runewidth.StringWidth(value) > width { value = truncateCell(value, width) diff --git a/internal/tui/tui_test.go b/internal/tui/tui_test.go index 9811f57..8f92c1f 100644 --- a/internal/tui/tui_test.go +++ b/internal/tui/tui_test.go @@ -7,6 +7,8 @@ import ( "github.com/Life-USTC/CLI/internal/output" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "github.com/fatih/color" + "github.com/muesli/termenv" ) func TestSearchResultFromInputs(t *testing.T) { @@ -136,6 +138,53 @@ func TestSearchTableEditReturnsToSearchWithoutQuitting(t *testing.T) { } } +func TestSearchTableEnterSelectsHighlightedRow(t *testing.T) { + m := newTableModel(SearchTable{ + OnSelect: func(map[string]any) error { return nil }, + }) + m.mode = tableModeResults + m.selected = 1 + m.result.Rows = []map[string]any{ + {"jwId": float64(101)}, + {"jwId": float64(202)}, + } + + updated, cmd := m.Update(tea.KeyMsg{Type: tea.KeyEnter}) + m = updated.(tableModel) + if cmd == nil { + t.Fatal("enter should quit the TUI before opening the selected result") + } + if _, ok := cmd().(tea.QuitMsg); !ok { + t.Fatalf("enter command = %T, want tea.QuitMsg", cmd()) + } + if got := m.selection["jwId"]; got != float64(202) { + t.Fatalf("selected jwId = %v, want 202", got) + } +} + +func TestSearchTableNoColorUsesAsciiProfile(t *testing.T) { + previousProfile := lipgloss.ColorProfile() + previousNoColor := output.Current.NoColor + previousFatihNoColor := color.NoColor + lipgloss.SetColorProfile(termenv.ANSI256) + output.Current.NoColor = true + color.NoColor = false + t.Cleanup(func() { + lipgloss.SetColorProfile(previousProfile) + output.Current.NoColor = previousNoColor + color.NoColor = previousFatihNoColor + }) + + restore := configureSearchTableColors() + if got := lipgloss.ColorProfile(); got != termenv.Ascii { + t.Fatalf("color profile = %v, want Ascii", got) + } + restore() + if got := lipgloss.ColorProfile(); got != termenv.ANSI256 { + t.Fatalf("restored color profile = %v, want ANSI256", got) + } +} + func TestSearchTableCanPageInsideResults(t *testing.T) { var got SearchResult m := newTableModel(SearchTable{ @@ -183,6 +232,7 @@ func TestSearchTableResultsFitCompactWindow(t *testing.T) { Fetch: func(SearchResult) (TableResult, error) { return TableResult{}, nil }, + OnSelect: func(map[string]any) error { return nil }, }) updated, _ := m.Update(tea.WindowSizeMsg{Width: 40, Height: 12}) m = updated.(tableModel) @@ -208,6 +258,69 @@ func TestSearchTableResultsFitCompactWindow(t *testing.T) { if renderedHeight := lipgloss.Height(rendered); renderedHeight > 12 { t.Fatalf("rendered height = %d, want <= 12\n%s", renderedHeight, rendered) } + if !strings.Contains(rendered, "enter open") { + t.Fatalf("compact results did not show the selection action:\n%s", rendered) + } +} + +func TestSearchTableSelectedCJKRowDoesNotWrapWithColor(t *testing.T) { + previousProfile := lipgloss.ColorProfile() + lipgloss.SetColorProfile(termenv.ANSI256) + t.Cleanup(func() { + lipgloss.SetColorProfile(previousProfile) + }) + + m := newTableModel(SearchTable{ + Columns: []output.Column{ + {Header: "Code", Key: "code"}, + {Header: "Name", Key: "namePrimary"}, + {Header: "Level", Key: "educationLevel.name"}, + {Header: "JW ID", Key: "jwId"}, + }, + OnSelect: func(map[string]any) error { return nil }, + }) + m.mode = tableModeResults + m.result = TableResult{ + Rows: []map[string]any{ + { + "code": "MATH101", + "namePrimary": "数学分析(B1)", + "educationLevel": map[string]any{"name": "Undergraduate"}, + "jwId": "course-jw-id-1", + }, + { + "code": "MATH102", + "namePrimary": "线性代数(A)", + "educationLevel": map[string]any{"name": "Undergraduate"}, + "jwId": "course-jw-id-2", + }, + }, + Total: 2, + Page: 1, + } + + for _, size := range []struct { + width int + height int + }{ + {width: 80, height: 24}, + {width: 40, height: 12}, + } { + updated, _ := m.Update(tea.WindowSizeMsg{Width: size.width, Height: size.height}) + sized := updated.(tableModel) + selected := sized.View() + sized.selected = len(sized.result.Rows) + unselected := sized.View() + if selectedHeight, unselectedHeight := lipgloss.Height(selected), lipgloss.Height(unselected); selectedHeight != unselectedHeight { + t.Fatalf("%dx%d selected view height = %d, unselected view height = %d\nselected:\n%s\nunselected:\n%s", size.width, size.height, selectedHeight, unselectedHeight, selected, unselected) + } + if selectedHeight := lipgloss.Height(selected); selectedHeight > size.height { + t.Fatalf("%dx%d selected view height = %d\n%s", size.width, size.height, selectedHeight, selected) + } + if selectedWidth := lipgloss.Width(selected); selectedWidth > size.width { + t.Fatalf("%dx%d selected view width = %d\n%s", size.width, size.height, selectedWidth, selected) + } + } } func TestSearchTableErrorFitsCompactWindow(t *testing.T) { diff --git a/tui-screenshots/course-80x20.png b/tui-screenshots/course-80x20.png deleted file mode 100644 index 81c723d..0000000 Binary files a/tui-screenshots/course-80x20.png and /dev/null differ diff --git a/tui-screenshots/section-80x20.png b/tui-screenshots/section-80x20.png deleted file mode 100644 index f637cb5..0000000 Binary files a/tui-screenshots/section-80x20.png and /dev/null differ diff --git a/tui-screenshots/teacher-40x12-compact.png b/tui-screenshots/teacher-40x12-compact.png deleted file mode 100644 index 3815a5d..0000000 Binary files a/tui-screenshots/teacher-40x12-compact.png and /dev/null differ diff --git a/tui-screenshots/teacher-80x20.png b/tui-screenshots/teacher-80x20.png deleted file mode 100644 index 2c00f4f..0000000 Binary files a/tui-screenshots/teacher-80x20.png and /dev/null differ