diff --git a/apps/spruce/src/gql/generated/types.ts b/apps/spruce/src/gql/generated/types.ts index d9e225593..5957baaf5 100644 --- a/apps/spruce/src/gql/generated/types.ts +++ b/apps/spruce/src/gql/generated/types.ts @@ -2230,7 +2230,7 @@ export type Query = { userSettings?: Maybe; version: Version; viewableProjectRefs: Array; - waterfall?: Maybe; + waterfall: Waterfall; }; export type QueryBbGetCreatedTicketsArgs = { @@ -3428,7 +3428,9 @@ export type VolumeHost = { export type Waterfall = { __typename?: "Waterfall"; buildVariants: Array; - versions: Array; + nextPageOrder: Scalars["Int"]["output"]; + prevPageOrder: Scalars["Int"]["output"]; + versions: Array; }; export type WaterfallBuild = { @@ -3449,6 +3451,10 @@ export type WaterfallBuildVariant = { export type WaterfallOptions = { limit?: InputMaybe; + /** Return versions with an order lower than maxOrder. Used for paginating forward. */ + maxOrder?: InputMaybe; + /** Return versions with an order greater than minOrder. Used for paginating backward. */ + minOrder?: InputMaybe; projectIdentifier: Scalars["String"]["input"]; requesters?: InputMaybe>; }; @@ -3460,6 +3466,12 @@ export type WaterfallTask = { status: Scalars["String"]["output"]; }; +export type WaterfallVersion = { + __typename?: "WaterfallVersion"; + inactiveVersions?: Maybe>; + version?: Maybe; +}; + export type Webhook = { __typename?: "Webhook"; endpoint: Scalars["String"]["output"]; diff --git a/apps/spruce/src/pages/task/taskTabs/ExecutionTasksTable.tsx b/apps/spruce/src/pages/task/taskTabs/ExecutionTasksTable.tsx index 5117122c4..ec8eacf19 100644 --- a/apps/spruce/src/pages/task/taskTabs/ExecutionTasksTable.tsx +++ b/apps/spruce/src/pages/task/taskTabs/ExecutionTasksTable.tsx @@ -136,10 +136,22 @@ const getInitialSorting = (queryParams: { }, ]; } else if (sorts) { - const parsedSorts = parseSortString(sorts); - initialSorting = parsedSorts.map(({ Direction, Key }) => ({ - id: Key as TaskSortCategory, - desc: Direction === SortDirection.Desc, + const parsedSorts = parseSortString< + "id", + "direction", + TaskSortCategory, + { + id: TaskSortCategory; + direction: SortDirection; + } + >(sorts, { + sortCategoryEnum: TaskSortCategory, + sortByKey: "id", + sortDirKey: "direction", + }); + initialSorting = parsedSorts.map(({ direction, id }) => ({ + id, + desc: direction === SortDirection.Desc, })); } diff --git a/apps/spruce/src/pages/task/taskTabs/TestsTable.tsx b/apps/spruce/src/pages/task/taskTabs/TestsTable.tsx index c5fa9b637..42b069fc9 100644 --- a/apps/spruce/src/pages/task/taskTabs/TestsTable.tsx +++ b/apps/spruce/src/pages/task/taskTabs/TestsTable.tsx @@ -24,6 +24,7 @@ import { TestSortCategory, TestResult, TaskQuery, + TestSortOptions, } from "gql/generated/types"; import { TASK_TESTS } from "gql/queries"; import { useTableSort, useUpdateURLQueryParams, usePolling } from "hooks"; @@ -266,12 +267,17 @@ const getQueryVariables = ( ? SortDirection.Desc : SortDirection.Asc; - // @ts-expect-error: FIXME. This comment was added by an automated script. - let sort = []; + let sort: TestSortOptions[] = []; if (sortBy && direction) { sort = [{ sortBy, direction }]; } else if (sorts) { - sort = parseSortString(sorts, { + sort = parseSortString< + "sortBy", + "direction", + TestSortCategory, + TestSortOptions + >(sorts, { + sortCategoryEnum: TestSortCategory, sortByKey: "sortBy", sortDirKey: "direction", }); @@ -286,7 +292,6 @@ const getQueryVariables = ( return { id: taskId, execution: queryParamAsNumber(execution), - // @ts-expect-error: FIXME. This comment was added by an automated script. sort, limitNum: getLimit(queryParams[PaginationQueryParams.Limit]), statusList, diff --git a/apps/spruce/src/pages/version/useQueryVariables/index.ts b/apps/spruce/src/pages/version/useQueryVariables/index.ts index 7f043d9f8..1699ac4de 100644 --- a/apps/spruce/src/pages/version/useQueryVariables/index.ts +++ b/apps/spruce/src/pages/version/useQueryVariables/index.ts @@ -27,12 +27,27 @@ export const useQueryVariables = ( // This should be reworked once the antd tables are removed. // At the current state, sorts & duration will never both be defined. - let sortsToApply: SortOrder[]; + let sortsToApply: SortOrder[] = []; + const opts = { + sortByKey: "Key" as "Key", + sortDirKey: "Direction" as "Direction", + sortCategoryEnum: TaskSortCategory, + }; if (sorts) { - sortsToApply = parseSortString(sorts); + sortsToApply = parseSortString< + "Key", + "Direction", + TaskSortCategory, + SortOrder + >(sorts, opts); } if (duration) { - sortsToApply = parseSortString(`${TaskSortCategory.Duration}:${duration}`); + sortsToApply = parseSortString< + "Key", + "Direction", + TaskSortCategory, + SortOrder + >(`${TaskSortCategory.Duration}:${duration}`, opts); } return { @@ -42,7 +57,6 @@ export const useQueryVariables = ( taskName: getString(taskName), statuses: toArray(statuses), baseStatuses: toArray(baseStatuses), - // @ts-expect-error: FIXME. This comment was added by an automated script. sorts: sortsToApply, limit, page, diff --git a/apps/spruce/src/utils/queryString/sortString.test.ts b/apps/spruce/src/utils/queryString/sortString.test.ts index 985ee90a1..371791be3 100644 --- a/apps/spruce/src/utils/queryString/sortString.test.ts +++ b/apps/spruce/src/utils/queryString/sortString.test.ts @@ -1,10 +1,24 @@ import { SorterResult } from "antd/es/table/interface"; -import { SortDirection, TaskSortCategory, Task } from "gql/generated/types"; +import { + SortDirection, + TaskSortCategory, + Task, + SortOrder, +} from "gql/generated/types"; import { parseSortString, toSortString } from "./sortString"; describe("parseSortString", () => { it("should parse a sort string with multiple sorts", () => { - expect(parseSortString("NAME:ASC;STATUS:DESC")).toStrictEqual([ + expect( + parseSortString<"Key", "Direction", TaskSortCategory, SortOrder>( + "NAME:ASC;STATUS:DESC", + { + sortByKey: "Key", + sortDirKey: "Direction", + sortCategoryEnum: TaskSortCategory, + }, + ), + ).toStrictEqual([ { Key: TaskSortCategory.Name, Direction: SortDirection.Asc, @@ -15,8 +29,56 @@ describe("parseSortString", () => { }, ]); }); - it("should not parse an invalid sort string", () => { - expect(parseSortString("FOO:ASC")).toStrictEqual([]); + enum Categories { + Apple = "apple", + Banana = "banana", + Pear = "pear", + } + it("should partially process invalid sort strings", () => { + expect( + parseSortString< + "cat", + "dir", + Categories, + { cat: Categories; dir: SortDirection } + >("apple:ASC;pear:DESC;invalidCat:DESC", { + sortByKey: "cat", + sortDirKey: "dir", + sortCategoryEnum: Categories, + }), + ).toStrictEqual([ + { + cat: Categories.Apple, + dir: SortDirection.Asc, + }, + { + cat: Categories.Pear, + dir: SortDirection.Desc, + }, + ]); + }); + it("can accept an array of strings", () => { + expect( + parseSortString< + "cat", + "dir", + Categories, + { cat: Categories; dir: SortDirection } + >(["apple:ASC", "pear:DESC"], { + sortByKey: "cat", + sortDirKey: "dir", + sortCategoryEnum: Categories, + }), + ).toStrictEqual([ + { + cat: Categories.Apple, + dir: SortDirection.Asc, + }, + { + cat: Categories.Pear, + dir: SortDirection.Desc, + }, + ]); }); }); diff --git a/apps/spruce/src/utils/queryString/sortString.ts b/apps/spruce/src/utils/queryString/sortString.ts index 80fc0facb..e201808c4 100644 --- a/apps/spruce/src/utils/queryString/sortString.ts +++ b/apps/spruce/src/utils/queryString/sortString.ts @@ -1,5 +1,5 @@ import { Key, SorterResult } from "antd/es/table/interface"; -import { Task, SortDirection, TaskSortCategory } from "gql/generated/types"; +import { Task, SortDirection } from "gql/generated/types"; export const getSortString = (columnKey: Key, direction: SortDirection) => columnKey && direction ? `${columnKey}:${direction}` : undefined; @@ -34,43 +34,54 @@ export const toSortString = ( : undefined; }; -// takes a sort query string and parses it into valid GQL params -// By default, uses keys for task's SortOrder type, but sort field keys can be passed in for use with e.g. tests' TestSortOptions +/** + * Parses a sort query string or array into an array of sort objects. + * The result is in the shape [{ [SortByKey]: SortCategoryEnum, [SortDirectionKey]: SortDirection }, ...] + * @template SortByKey - The key for the sort category. + * @template SortDirectionKey - The key for the sort direction. + * @template SortCategoryEnum - The enum type for sort categories. + * @template T - The type of the resulting sort objects, which must include + * both the sort category and direction. + * @param sortQuery - A string or an array of strings representing the sort + * criteria in the format "category:direction". + * @param options - An object containing the following properties: + * @param options.sortByKey - The key to use for the sort category in the resulting objects. + * @param options.sortDirKey - The key to use for the sort direction in the resulting objects. + * @param options.sortCategoryEnum - An object that maps valid sort categories to their + * corresponding enum values. + * @returns An array of sort objects, each containing the specified sort + * category and direction. + */ export const parseSortString = < - T extends Record, + SortByKey extends string, + SortDirectionKey extends string, + SortCategoryEnum extends string, + T extends Record & + Record, >( sortQuery: string | string[], options: { - sortByKey: keyof T; - sortDirKey: keyof T; - } = { sortByKey: "Key", sortDirKey: "Direction" }, + sortByKey: SortByKey; + sortDirKey: SortDirectionKey; + sortCategoryEnum: Record; + }, ): T[] => { - let sorts: T[] = []; - let sortArray: string[] = []; - if (typeof sortQuery === "string") { - sortArray = sortQuery.split(";"); - } else { - sortArray = sortQuery; - } - if (sortArray?.length > 0) { - sortArray.forEach((singleSort) => { - const parts = singleSort.split(":"); - if (parts.length !== 2) { - return; - } - if ( - !Object.values(TaskSortCategory).includes(parts[0] as TaskSortCategory) - ) { - return; - } - if (!Object.values(SortDirection).includes(parts[1] as SortDirection)) { - return; - } - sorts = sorts.concat({ - [options.sortByKey]: parts[0] as TaskSortCategory, - [options.sortDirKey]: parts[1] as SortDirection, + const { sortByKey, sortCategoryEnum, sortDirKey } = options; + const sortArray = Array.isArray(sortQuery) ? sortQuery : sortQuery.split(";"); + const sorts: T[] = sortArray.reduce((accum: T[], singleSort: string) => { + const [category, direction] = singleSort.split(":"); + if ( + category && + direction && + Object.values(sortCategoryEnum).includes(category as SortCategoryEnum) && + Object.values(SortDirection).includes(direction as SortDirection) + ) { + accum.push({ + [sortByKey]: category as SortCategoryEnum, + [sortDirKey]: direction as SortDirection, } as T); - }); - } + } + return accum; + }, []); return sorts; };