From 75f3f92f04287b58f42f9a62cba88dc6ac7dc749 Mon Sep 17 00:00:00 2001 From: Sacha Al Himdani Date: Tue, 28 Oct 2025 16:38:12 +0100 Subject: [PATCH] Add rank to reference Signed-off-by: Sacha Al Himdani --- .../TrustCenterReferenceDialog.tsx | 21 ++++ .../TrustCenterReferencesSection.tsx | 98 ++++++++++++++- .../hooks/graph/TrustCenterReferenceGraph.ts | 35 +++++- ...terReferenceGraphCreateMutation.graphql.ts | 18 ++- .../TrustCenterReferenceGraphQuery.graphql.ts | 24 ++-- ...terReferenceGraphUpdateMutation.graphql.ts | 17 ++- ...eferenceGraphUpdateRankMutation.graphql.ts | 118 ++++++++++++++++++ pkg/coredata/migrations/20251028T152339Z.sql | 19 +++ pkg/coredata/trust_center_reference.go | 90 ++++++++++--- .../trust_center_reference_order_field.go | 3 + pkg/probo/trust_center_reference_service.go | 8 ++ pkg/server/api/console/v1/schema.graphql | 6 + pkg/server/api/console/v1/schema/schema.go | 78 +++++++++++- .../v1/types/trust_center_reference.go | 1 + pkg/server/api/console/v1/types/types.go | 2 + pkg/server/api/console/v1/v1_resolver.go | 5 +- pkg/server/api/trust/v1/v1_resolver.go | 4 +- 17 files changed, 497 insertions(+), 50 deletions(-) create mode 100644 apps/console/src/hooks/graph/__generated__/TrustCenterReferenceGraphUpdateRankMutation.graphql.ts create mode 100644 pkg/coredata/migrations/20251028T152339Z.sql diff --git a/apps/console/src/components/trustCenter/TrustCenterReferenceDialog.tsx b/apps/console/src/components/trustCenter/TrustCenterReferenceDialog.tsx index 8713914f2..82f47cecd 100644 --- a/apps/console/src/components/trustCenter/TrustCenterReferenceDialog.tsx +++ b/apps/console/src/components/trustCenter/TrustCenterReferenceDialog.tsx @@ -22,6 +22,7 @@ const referenceSchema = z.object({ name: z.string().min(1, "Name is required"), description: z.string(), websiteUrl: z.string().url("Please enter a valid URL"), + rank: z.number().int().positive().optional(), }); type ReferenceFormData = z.infer; @@ -33,6 +34,7 @@ export type TrustCenterReferenceDialogRef = { name: string; description: string; websiteUrl: string; + rank: number; }) => void; }; @@ -41,6 +43,7 @@ type Reference = { name: string; description: string; websiteUrl: string; + rank: number; }; export const TrustCenterReferenceDialog = forwardRef( @@ -89,6 +92,7 @@ export const TrustCenterReferenceDialog = forwardRef = {}; if (uploadedFile) { @@ -210,6 +219,18 @@ export const TrustCenterReferenceDialog = forwardRef + {mode === 'edit' && ( + + )} + (null); - const data = useTrustCenterReferences(trustCenterId); + const [draggedIndex, setDraggedIndex] = useState(null); + const [dragOverIndex, setDragOverIndex] = useState(null); + const [refetchKey, setRefetchKey] = useState(0); + const data = useTrustCenterReferences(trustCenterId, refetchKey); + const [updateRank] = useUpdateTrustCenterReferenceRankMutation(); const trustCenterNode = data?.node; const references = trustCenterNode?.references?.edges?.map((edge) => edge.node) || []; @@ -55,11 +61,47 @@ export function TrustCenterReferencesSection({ trustCenterId }: Props) { dialogRef.current?.openEdit(reference); }; - const handleVisitWebsite = (websiteUrl: string) => { safeOpenUrl(websiteUrl); }; + const handleDragStart = (index: number) => { + setDraggedIndex(index); + }; + + const handleDragOver = (e: React.DragEvent, index: number) => { + e.preventDefault(); + if (draggedIndex !== index) { + setDragOverIndex(index); + } + }; + + const handleDrop = (targetIndex: number) => { + if (draggedIndex === null || draggedIndex === targetIndex) { + setDraggedIndex(null); + setDragOverIndex(null); + return; + } + + const draggedRef = references[draggedIndex]; + const targetRank = references[targetIndex].rank; + + updateRank({ + variables: { + input: { + id: draggedRef.id, + rank: targetRank, + }, + }, + onCompleted: () => { + setRefetchKey((prev) => prev + 1); + }, + }); + + setDraggedIndex(null); + setDragOverIndex(null); + }; + return (
@@ -93,18 +135,28 @@ export function TrustCenterReferencesSection({ trustCenterId }: Props) { )} - {references.map((reference: Reference) => ( + {references.map((reference: Reference, index: number) => ( handleEdit(reference)} connectionId={referencesConnectionId} onVisitWebsite={() => handleVisitWebsite(reference.websiteUrl)} + onDragStart={() => handleDragStart(index)} + onDragOver={(e) => handleDragOver(e, index)} + onDrop={() => handleDrop(index)} /> ))} +

+ {__("Drag and drop references to change their displayed order")} +

+
); @@ -112,14 +164,48 @@ export function TrustCenterReferencesSection({ trustCenterId }: Props) { type ReferenceRowProps = { reference: Reference; + index: number; + isDragging: boolean; + isDropTarget: boolean; onEdit: () => void; connectionId: string; onVisitWebsite: () => void; + onDragStart: () => void; + onDragOver: (e: React.DragEvent) => void; + onDrop: () => void; }; -function ReferenceRow({ reference, onEdit, connectionId, onVisitWebsite }: ReferenceRowProps) { +function ReferenceRow({ + reference, + isDragging, + isDropTarget, + onEdit, + connectionId, + onVisitWebsite, + onDragStart, + onDragOver, + onDrop, +}: ReferenceRowProps) { + const [isMouseDown, setIsMouseDown] = useState(false); + + const className = [ + isDragging && "opacity-50 cursor-grabbing", + !isDragging && !isMouseDown && "cursor-grab", + !isDragging && isMouseDown && "cursor-grabbing", + isDropTarget && "!bg-primary-50 border-y-2 border-primary-500", + ].filter(Boolean).join(" "); + return ( - + setIsMouseDown(true)} + onMouseUp={() => setIsMouseDown(false)} + onMouseLeave={() => setIsMouseDown(false)} + className={className} + >
( trustCenterReferencesQuery, { trustCenterId: trustCenterId || "" }, - { fetchPolicy: 'network-only' } + { fetchPolicy: 'network-only', fetchKey: refetchKey } ); return trustCenterId ? data : null; @@ -122,6 +126,29 @@ export function useUpdateTrustCenterReferenceMutation() { ); } +export const updateTrustCenterReferenceRankMutation = graphql` + mutation TrustCenterReferenceGraphUpdateRankMutation( + $input: UpdateTrustCenterReferenceInput! + ) { + updateTrustCenterReference(input: $input) { + trustCenterReference { + id + rank + } + } + } +`; + +export function useUpdateTrustCenterReferenceRankMutation() { + return useMutationWithToasts( + updateTrustCenterReferenceRankMutation, + { + successMessage: "Order updated successfully", + errorMessage: "Failed to update order", + } + ); +} + export function useDeleteTrustCenterReferenceMutation() { return useMutationWithToasts( deleteTrustCenterReferenceMutation, diff --git a/apps/console/src/hooks/graph/__generated__/TrustCenterReferenceGraphCreateMutation.graphql.ts b/apps/console/src/hooks/graph/__generated__/TrustCenterReferenceGraphCreateMutation.graphql.ts index 92c5be0a6..c94e86e47 100644 --- a/apps/console/src/hooks/graph/__generated__/TrustCenterReferenceGraphCreateMutation.graphql.ts +++ b/apps/console/src/hooks/graph/__generated__/TrustCenterReferenceGraphCreateMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<216f3e7b4510c3d1127c3910bebc8b95>> + * @generated SignedSource<<9d17f089c36e0567d217fe339b6382fd>> * @lightSyntaxTransform * @nogrep */ @@ -30,6 +30,7 @@ export type TrustCenterReferenceGraphCreateMutation$data = { readonly id: string; readonly logoUrl: string; readonly name: string; + readonly rank: number; readonly updatedAt: any; readonly websiteUrl: string; }; @@ -117,6 +118,13 @@ v3 = { "name": "logoUrl", "storageKey": null }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "rank", + "storageKey": null + }, { "alias": null, "args": null, @@ -185,7 +193,7 @@ return { "alias": null, "args": null, "filters": null, - "handle": "prependEdge", + "handle": "appendEdge", "key": "", "kind": "LinkedHandle", "name": "trustCenterReferenceEdge", @@ -203,16 +211,16 @@ return { ] }, "params": { - "cacheID": "17a206ba4702d641ffd7631cb8e3e03b", + "cacheID": "623c9df0fba82887a878addd1ddaf706", "id": null, "metadata": {}, "name": "TrustCenterReferenceGraphCreateMutation", "operationKind": "mutation", - "text": "mutation TrustCenterReferenceGraphCreateMutation(\n $input: CreateTrustCenterReferenceInput!\n) {\n createTrustCenterReference(input: $input) {\n trustCenterReferenceEdge {\n cursor\n node {\n id\n name\n description\n websiteUrl\n logoUrl\n createdAt\n updatedAt\n }\n }\n }\n}\n" + "text": "mutation TrustCenterReferenceGraphCreateMutation(\n $input: CreateTrustCenterReferenceInput!\n) {\n createTrustCenterReference(input: $input) {\n trustCenterReferenceEdge {\n cursor\n node {\n id\n name\n description\n websiteUrl\n logoUrl\n rank\n createdAt\n updatedAt\n }\n }\n }\n}\n" } }; })(); -(node as any).hash = "e0a76c3f5582bf1ed5def08db36d9e25"; +(node as any).hash = "ee754e6c9bc3aa992e1a40e828b0d07e"; export default node; diff --git a/apps/console/src/hooks/graph/__generated__/TrustCenterReferenceGraphQuery.graphql.ts b/apps/console/src/hooks/graph/__generated__/TrustCenterReferenceGraphQuery.graphql.ts index 46883de1d..ebe7ba952 100644 --- a/apps/console/src/hooks/graph/__generated__/TrustCenterReferenceGraphQuery.graphql.ts +++ b/apps/console/src/hooks/graph/__generated__/TrustCenterReferenceGraphQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<85556d15b776b3e61e312057a994d482>> * @lightSyntaxTransform * @nogrep */ @@ -25,6 +25,7 @@ export type TrustCenterReferenceGraphQuery$data = { readonly id: string; readonly logoUrl: string; readonly name: string; + readonly rank: number; readonly updatedAt: any; readonly websiteUrl: string; }; @@ -69,8 +70,8 @@ v3 = { "kind": "Literal", "name": "orderBy", "value": { - "direction": "DESC", - "field": "CREATED_AT" + "direction": "ASC", + "field": "RANK" } }, v4 = { @@ -172,6 +173,13 @@ v5 = [ "name": "logoUrl", "storageKey": null }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "rank", + "storageKey": null + }, { "alias": null, "args": null, @@ -243,7 +251,7 @@ return { "name": "__TrustCenterReferencesSection_references_connection", "plural": false, "selections": (v5/*: any*/), - "storageKey": "__TrustCenterReferencesSection_references_connection(orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})" + "storageKey": "__TrustCenterReferencesSection_references_connection(orderBy:{\"direction\":\"ASC\",\"field\":\"RANK\"})" } ], "type": "TrustCenter", @@ -283,7 +291,7 @@ return { "name": "references", "plural": false, "selections": (v5/*: any*/), - "storageKey": "references(first:100,orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})" + "storageKey": "references(first:100,orderBy:{\"direction\":\"ASC\",\"field\":\"RANK\"})" }, { "alias": null, @@ -306,7 +314,7 @@ return { ] }, "params": { - "cacheID": "8e9fd111488feb14deeedf47bfa4a0f0", + "cacheID": "8fdd97abcaa0d9eb889eb454c85f2e4c", "id": null, "metadata": { "connection": [ @@ -323,11 +331,11 @@ return { }, "name": "TrustCenterReferenceGraphQuery", "operationKind": "query", - "text": "query TrustCenterReferenceGraphQuery(\n $trustCenterId: ID!\n) {\n node(id: $trustCenterId) {\n __typename\n ... on TrustCenter {\n id\n references(first: 100, orderBy: {field: CREATED_AT, direction: DESC}) {\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n edges {\n cursor\n node {\n id\n name\n description\n websiteUrl\n logoUrl\n createdAt\n updatedAt\n __typename\n }\n }\n }\n }\n id\n }\n}\n" + "text": "query TrustCenterReferenceGraphQuery(\n $trustCenterId: ID!\n) {\n node(id: $trustCenterId) {\n __typename\n ... on TrustCenter {\n id\n references(first: 100, orderBy: {field: RANK, direction: ASC}) {\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n edges {\n cursor\n node {\n id\n name\n description\n websiteUrl\n logoUrl\n rank\n createdAt\n updatedAt\n __typename\n }\n }\n }\n }\n id\n }\n}\n" } }; })(); -(node as any).hash = "8b62ed10055cff04e117a31dd598eee4"; +(node as any).hash = "cb4b9b8a68249ae8066a3ff2bfc6acd8"; export default node; diff --git a/apps/console/src/hooks/graph/__generated__/TrustCenterReferenceGraphUpdateMutation.graphql.ts b/apps/console/src/hooks/graph/__generated__/TrustCenterReferenceGraphUpdateMutation.graphql.ts index 22283f14b..4d7e78048 100644 --- a/apps/console/src/hooks/graph/__generated__/TrustCenterReferenceGraphUpdateMutation.graphql.ts +++ b/apps/console/src/hooks/graph/__generated__/TrustCenterReferenceGraphUpdateMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<3b02f7734c467adddc8f8abe1c18c4e5>> + * @generated SignedSource<<9f21158abea504110ef50d4e0114a56a>> * @lightSyntaxTransform * @nogrep */ @@ -14,6 +14,7 @@ export type UpdateTrustCenterReferenceInput = { id: string; logoFile?: any | null | undefined; name?: string | null | undefined; + rank?: number | null | undefined; websiteUrl?: string | null | undefined; }; export type TrustCenterReferenceGraphUpdateMutation$variables = { @@ -27,6 +28,7 @@ export type TrustCenterReferenceGraphUpdateMutation$data = { readonly id: string; readonly logoUrl: string; readonly name: string; + readonly rank: number; readonly updatedAt: any; readonly websiteUrl: string; }; @@ -103,6 +105,13 @@ v1 = [ "name": "logoUrl", "storageKey": null }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "rank", + "storageKey": null + }, { "alias": null, "args": null, @@ -142,16 +151,16 @@ return { "selections": (v1/*: any*/) }, "params": { - "cacheID": "e5fa3bdb21897523c1491d6cfbf816cf", + "cacheID": "f55510d6d1e0a686d4733f5bd82a605a", "id": null, "metadata": {}, "name": "TrustCenterReferenceGraphUpdateMutation", "operationKind": "mutation", - "text": "mutation TrustCenterReferenceGraphUpdateMutation(\n $input: UpdateTrustCenterReferenceInput!\n) {\n updateTrustCenterReference(input: $input) {\n trustCenterReference {\n id\n name\n description\n websiteUrl\n logoUrl\n createdAt\n updatedAt\n }\n }\n}\n" + "text": "mutation TrustCenterReferenceGraphUpdateMutation(\n $input: UpdateTrustCenterReferenceInput!\n) {\n updateTrustCenterReference(input: $input) {\n trustCenterReference {\n id\n name\n description\n websiteUrl\n logoUrl\n rank\n createdAt\n updatedAt\n }\n }\n}\n" } }; })(); -(node as any).hash = "2340a9c559d302025df08a5748c18b80"; +(node as any).hash = "bed2a3190570cd85d85dd38f20f375da"; export default node; diff --git a/apps/console/src/hooks/graph/__generated__/TrustCenterReferenceGraphUpdateRankMutation.graphql.ts b/apps/console/src/hooks/graph/__generated__/TrustCenterReferenceGraphUpdateRankMutation.graphql.ts new file mode 100644 index 000000000..d915e8934 --- /dev/null +++ b/apps/console/src/hooks/graph/__generated__/TrustCenterReferenceGraphUpdateRankMutation.graphql.ts @@ -0,0 +1,118 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type UpdateTrustCenterReferenceInput = { + description?: string | null | undefined; + id: string; + logoFile?: any | null | undefined; + name?: string | null | undefined; + rank?: number | null | undefined; + websiteUrl?: string | null | undefined; +}; +export type TrustCenterReferenceGraphUpdateRankMutation$variables = { + input: UpdateTrustCenterReferenceInput; +}; +export type TrustCenterReferenceGraphUpdateRankMutation$data = { + readonly updateTrustCenterReference: { + readonly trustCenterReference: { + readonly id: string; + readonly rank: number; + }; + }; +}; +export type TrustCenterReferenceGraphUpdateRankMutation = { + response: TrustCenterReferenceGraphUpdateRankMutation$data; + variables: TrustCenterReferenceGraphUpdateRankMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" + } +], +v1 = [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } + ], + "concreteType": "UpdateTrustCenterReferencePayload", + "kind": "LinkedField", + "name": "updateTrustCenterReference", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "TrustCenterReference", + "kind": "LinkedField", + "name": "trustCenterReference", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "rank", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "TrustCenterReferenceGraphUpdateRankMutation", + "selections": (v1/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "TrustCenterReferenceGraphUpdateRankMutation", + "selections": (v1/*: any*/) + }, + "params": { + "cacheID": "9e3d186eff2bff3d66482ab88f27aead", + "id": null, + "metadata": {}, + "name": "TrustCenterReferenceGraphUpdateRankMutation", + "operationKind": "mutation", + "text": "mutation TrustCenterReferenceGraphUpdateRankMutation(\n $input: UpdateTrustCenterReferenceInput!\n) {\n updateTrustCenterReference(input: $input) {\n trustCenterReference {\n id\n rank\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "72d23bac1e404137890be27bf98bc59e"; + +export default node; diff --git a/pkg/coredata/migrations/20251028T152339Z.sql b/pkg/coredata/migrations/20251028T152339Z.sql new file mode 100644 index 000000000..e85dcc2c9 --- /dev/null +++ b/pkg/coredata/migrations/20251028T152339Z.sql @@ -0,0 +1,19 @@ +ALTER TABLE trust_center_references ADD COLUMN rank INTEGER; + +WITH ranked_references AS ( + SELECT + id, + ROW_NUMBER() OVER (PARTITION BY trust_center_id ORDER BY created_at DESC, id DESC) AS rn + FROM trust_center_references +) +UPDATE trust_center_references tcr +SET rank = rr.rn +FROM ranked_references rr +WHERE tcr.id = rr.id; + +ALTER TABLE trust_center_references ALTER COLUMN rank SET NOT NULL; + +ALTER TABLE trust_center_references +ADD CONSTRAINT trust_center_references_trust_center_id_rank_key + UNIQUE (trust_center_id, rank) + DEFERRABLE INITIALLY DEFERRED; diff --git a/pkg/coredata/trust_center_reference.go b/pkg/coredata/trust_center_reference.go index f24064749..124973d46 100644 --- a/pkg/coredata/trust_center_reference.go +++ b/pkg/coredata/trust_center_reference.go @@ -34,15 +34,26 @@ type ( Description string `db:"description"` WebsiteURL string `db:"website_url"` LogoFileID gid.GID `db:"logo_file_id"` + Rank int `db:"rank"` CreatedAt time.Time `db:"created_at"` UpdatedAt time.Time `db:"updated_at"` } TrustCenterReferences []*TrustCenterReference + + ErrTrustCenterReferenceNotFound struct { + ID string + } ) +func (e ErrTrustCenterReferenceNotFound) Error() string { + return fmt.Sprintf("trust center reference not found: %s", e.ID) +} + func (t TrustCenterReference) CursorKey(orderBy TrustCenterReferenceOrderField) page.CursorKey { switch orderBy { + case TrustCenterReferenceOrderFieldRank: + return page.NewCursorKey(t.ID, t.Rank) case TrustCenterReferenceOrderFieldName: return page.NewCursorKey(t.ID, t.Name) case TrustCenterReferenceOrderFieldCreatedAt: @@ -67,6 +78,7 @@ SELECT description, website_url, logo_file_id, + rank, created_at, updated_at FROM @@ -96,7 +108,7 @@ LIMIT 1; return nil } -func (t TrustCenterReference) Insert( +func (t *TrustCenterReference) Insert( ctx context.Context, conn pg.Conn, scope Scoper, @@ -111,6 +123,7 @@ INSERT INTO description, website_url, logo_file_id, + rank, created_at, updated_at ) @@ -122,9 +135,11 @@ VALUES ( @description, @website_url, @logo_file_id, + (SELECT COALESCE(MAX(rank), 0) + 1 FROM trust_center_references WHERE trust_center_id = @trust_center_id), @created_at, @updated_at -); +) +RETURNING rank; ` args := pgx.StrictNamedArgs{ @@ -139,7 +154,7 @@ VALUES ( "updated_at": t.UpdatedAt, } - _, err := conn.Exec(ctx, q, args) + err := conn.QueryRow(ctx, q, args).Scan(&t.Rank) if err != nil { return fmt.Errorf("cannot insert trust center reference: %w", err) } @@ -162,16 +177,7 @@ SET updated_at = @updated_at WHERE %s - AND id = @id -RETURNING - id, - trust_center_id, - name, - description, - website_url, - logo_file_id, - created_at, - updated_at + AND id = @id; ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -186,17 +192,64 @@ RETURNING } maps.Copy(args, scope.SQLArguments()) - rows, err := conn.Query(ctx, q, args) + result, err := conn.Exec(ctx, q, args) if err != nil { return fmt.Errorf("cannot update trust center reference: %w", err) } - reference, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[TrustCenterReference]) - if err != nil { - return fmt.Errorf("cannot collect updated trust center reference: %w", err) + if result.RowsAffected() == 0 { + return ErrTrustCenterReferenceNotFound{ID: t.ID.String()} } - *t = reference + return nil +} + +func (t *TrustCenterReference) UpdateRank( + ctx context.Context, + conn pg.Conn, + scope Scoper, +) error { + q := ` +WITH old AS ( + SELECT + rank AS old_rank + FROM trust_center_references + WHERE %s AND id = @id AND trust_center_id = @trust_center_id +) + +UPDATE trust_center_references +SET + rank = CASE + WHEN id = @id THEN @new_rank + ELSE rank + CASE + WHEN @new_rank < old.old_rank THEN 1 + WHEN @new_rank > old.old_rank THEN -1 + END + END, + updated_at = @updated_at +FROM old +WHERE %s + AND ( + id = @id + OR (rank BETWEEN LEAST(old.old_rank, @new_rank) AND GREATEST(old.old_rank, @new_rank)) + ); +` + + scopeFragment := scope.SQLFragment() + q = fmt.Sprintf(q, scopeFragment, scopeFragment) + + args := pgx.StrictNamedArgs{ + "id": t.ID, + "new_rank": t.Rank, + "trust_center_id": t.TrustCenterID, + "updated_at": t.UpdatedAt, + } + maps.Copy(args, scope.SQLArguments()) + + _, err := conn.Exec(ctx, q, args) + if err != nil { + return fmt.Errorf("cannot update trust center reference rank: %w", err) + } return nil } @@ -242,6 +295,7 @@ SELECT description, website_url, logo_file_id, + rank, created_at, updated_at FROM diff --git a/pkg/coredata/trust_center_reference_order_field.go b/pkg/coredata/trust_center_reference_order_field.go index f2de4b42d..14db2f94f 100644 --- a/pkg/coredata/trust_center_reference_order_field.go +++ b/pkg/coredata/trust_center_reference_order_field.go @@ -19,6 +19,7 @@ type ( ) const ( + TrustCenterReferenceOrderFieldRank TrustCenterReferenceOrderField = "RANK" TrustCenterReferenceOrderFieldName TrustCenterReferenceOrderField = "NAME" TrustCenterReferenceOrderFieldCreatedAt TrustCenterReferenceOrderField = "CREATED_AT" TrustCenterReferenceOrderFieldUpdatedAt TrustCenterReferenceOrderField = "UPDATED_AT" @@ -26,6 +27,8 @@ const ( func (p TrustCenterReferenceOrderField) Column() string { switch p { + case TrustCenterReferenceOrderFieldRank: + return "rank" case TrustCenterReferenceOrderFieldName: return "name" case TrustCenterReferenceOrderFieldCreatedAt: diff --git a/pkg/probo/trust_center_reference_service.go b/pkg/probo/trust_center_reference_service.go index ee49def89..0b6559f42 100644 --- a/pkg/probo/trust_center_reference_service.go +++ b/pkg/probo/trust_center_reference_service.go @@ -52,6 +52,7 @@ type ( Description *string WebsiteURL *string LogoFile *File + Rank *int } DeleteTrustCenterReferenceRequest struct { @@ -229,6 +230,13 @@ func (s TrustCenterReferenceService) Update( } reference.UpdatedAt = now + if req.Rank != nil { + reference.Rank = *req.Rank + if err := reference.UpdateRank(ctx, tx, s.svc.scope); err != nil { + return fmt.Errorf("cannot update rank: %w", err) + } + } + if err := reference.Update(ctx, tx, s.svc.scope); err != nil { return fmt.Errorf("cannot update trust center reference: %w", err) } diff --git a/pkg/server/api/console/v1/schema.graphql b/pkg/server/api/console/v1/schema.graphql index 0453145ca..ad0b62fc1 100644 --- a/pkg/server/api/console/v1/schema.graphql +++ b/pkg/server/api/console/v1/schema.graphql @@ -1173,6 +1173,10 @@ enum TrustCenterReferenceOrderField @goModel( model: "github.com/getprobo/probo/pkg/coredata.TrustCenterReferenceOrderField" ) { + RANK + @goEnum( + value: "github.com/getprobo/probo/pkg/coredata.TrustCenterReferenceOrderFieldRank" + ) NAME @goEnum( value: "github.com/getprobo/probo/pkg/coredata.TrustCenterReferenceOrderFieldName" @@ -2461,6 +2465,7 @@ type TrustCenterReference implements Node { description: String! websiteUrl: String! logoUrl: String! @goField(forceResolver: true) + rank: Int! createdAt: Datetime! updatedAt: Datetime! } @@ -3252,6 +3257,7 @@ input UpdateTrustCenterReferenceInput { description: String websiteUrl: String logoFile: Upload + rank: Int } input DeleteTrustCenterReferenceInput { diff --git a/pkg/server/api/console/v1/schema/schema.go b/pkg/server/api/console/v1/schema/schema.go index 1a8467ae7..6f040435d 100644 --- a/pkg/server/api/console/v1/schema/schema.go +++ b/pkg/server/api/console/v1/schema/schema.go @@ -1366,6 +1366,7 @@ type ComplexityRoot struct { ID func(childComplexity int) int LogoURL func(childComplexity int) int Name func(childComplexity int) int + Rank func(childComplexity int) int UpdatedAt func(childComplexity int) int WebsiteURL func(childComplexity int) int } @@ -7937,6 +7938,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.TrustCenterReference.Name(childComplexity), true + case "TrustCenterReference.rank": + if e.complexity.TrustCenterReference.Rank == nil { + break + } + + return e.complexity.TrustCenterReference.Rank(childComplexity), true + case "TrustCenterReference.updatedAt": if e.complexity.TrustCenterReference.UpdatedAt == nil { break @@ -10491,6 +10499,10 @@ enum TrustCenterReferenceOrderField @goModel( model: "github.com/getprobo/probo/pkg/coredata.TrustCenterReferenceOrderField" ) { + RANK + @goEnum( + value: "github.com/getprobo/probo/pkg/coredata.TrustCenterReferenceOrderFieldRank" + ) NAME @goEnum( value: "github.com/getprobo/probo/pkg/coredata.TrustCenterReferenceOrderFieldName" @@ -11779,6 +11791,7 @@ type TrustCenterReference implements Node { description: String! websiteUrl: String! logoUrl: String! @goField(forceResolver: true) + rank: Int! createdAt: Datetime! updatedAt: Datetime! } @@ -12570,6 +12583,7 @@ input UpdateTrustCenterReferenceInput { description: String websiteUrl: String logoFile: Upload + rank: Int } input DeleteTrustCenterReferenceInput { @@ -59663,6 +59677,50 @@ func (ec *executionContext) fieldContext_TrustCenterReference_logoUrl(_ context. return fc, nil } +func (ec *executionContext) _TrustCenterReference_rank(ctx context.Context, field graphql.CollectedField, obj *types.TrustCenterReference) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_TrustCenterReference_rank(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return obj.Rank, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(int) + fc.Result = res + return ec.marshalNInt2int(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_TrustCenterReference_rank(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "TrustCenterReference", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _TrustCenterReference_createdAt(ctx context.Context, field graphql.CollectedField, obj *types.TrustCenterReference) (ret graphql.Marshaler) { fc, err := ec.fieldContext_TrustCenterReference_createdAt(ctx, field) if err != nil { @@ -59992,6 +60050,8 @@ func (ec *executionContext) fieldContext_TrustCenterReferenceEdge_node(_ context return ec.fieldContext_TrustCenterReference_websiteUrl(ctx, field) case "logoUrl": return ec.fieldContext_TrustCenterReference_logoUrl(ctx, field) + case "rank": + return ec.fieldContext_TrustCenterReference_rank(ctx, field) case "createdAt": return ec.fieldContext_TrustCenterReference_createdAt(ctx, field) case "updatedAt": @@ -61510,6 +61570,8 @@ func (ec *executionContext) fieldContext_UpdateTrustCenterReferencePayload_trust return ec.fieldContext_TrustCenterReference_websiteUrl(ctx, field) case "logoUrl": return ec.fieldContext_TrustCenterReference_logoUrl(ctx, field) + case "rank": + return ec.fieldContext_TrustCenterReference_rank(ctx, field) case "createdAt": return ec.fieldContext_TrustCenterReference_createdAt(ctx, field) case "updatedAt": @@ -76457,7 +76519,7 @@ func (ec *executionContext) unmarshalInputUpdateTrustCenterReferenceInput(ctx co asMap[k] = v } - fieldsInOrder := [...]string{"id", "name", "description", "websiteUrl", "logoFile"} + fieldsInOrder := [...]string{"id", "name", "description", "websiteUrl", "logoFile", "rank"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -76499,6 +76561,13 @@ func (ec *executionContext) unmarshalInputUpdateTrustCenterReferenceInput(ctx co return it, err } it.LogoFile = data + case "rank": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("rank")) + data, err := ec.unmarshalOInt2ᚖint(ctx, v) + if err != nil { + return it, err + } + it.Rank = data } } @@ -91027,6 +91096,11 @@ func (ec *executionContext) _TrustCenterReference(ctx context.Context, sel ast.S } out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "rank": + out.Values[i] = ec._TrustCenterReference_rank(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } case "createdAt": out.Values[i] = ec._TrustCenterReference_createdAt(ctx, field, obj) if out.Values[i] == graphql.Null { @@ -101438,11 +101512,13 @@ func (ec *executionContext) marshalNTrustCenterReferenceOrderField2githubᚗcom var ( unmarshalNTrustCenterReferenceOrderField2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐTrustCenterReferenceOrderField = map[string]coredata.TrustCenterReferenceOrderField{ + "RANK": coredata.TrustCenterReferenceOrderFieldRank, "NAME": coredata.TrustCenterReferenceOrderFieldName, "CREATED_AT": coredata.TrustCenterReferenceOrderFieldCreatedAt, "UPDATED_AT": coredata.TrustCenterReferenceOrderFieldUpdatedAt, } marshalNTrustCenterReferenceOrderField2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐTrustCenterReferenceOrderField = map[coredata.TrustCenterReferenceOrderField]string{ + coredata.TrustCenterReferenceOrderFieldRank: "RANK", coredata.TrustCenterReferenceOrderFieldName: "NAME", coredata.TrustCenterReferenceOrderFieldCreatedAt: "CREATED_AT", coredata.TrustCenterReferenceOrderFieldUpdatedAt: "UPDATED_AT", diff --git a/pkg/server/api/console/v1/types/trust_center_reference.go b/pkg/server/api/console/v1/types/trust_center_reference.go index fed052e68..7b5022e2d 100644 --- a/pkg/server/api/console/v1/types/trust_center_reference.go +++ b/pkg/server/api/console/v1/types/trust_center_reference.go @@ -35,6 +35,7 @@ func NewTrustCenterReference(tcc *coredata.TrustCenterReference) *TrustCenterRef Name: tcc.Name, Description: tcc.Description, WebsiteURL: tcc.WebsiteURL, + Rank: tcc.Rank, CreatedAt: tcc.CreatedAt, UpdatedAt: tcc.UpdatedAt, } diff --git a/pkg/server/api/console/v1/types/types.go b/pkg/server/api/console/v1/types/types.go index fc5dde85f..204921333 100644 --- a/pkg/server/api/console/v1/types/types.go +++ b/pkg/server/api/console/v1/types/types.go @@ -1734,6 +1734,7 @@ type TrustCenterReference struct { Description string `json:"description"` WebsiteURL string `json:"websiteUrl"` LogoURL string `json:"logoUrl"` + Rank int `json:"rank"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } @@ -2024,6 +2025,7 @@ type UpdateTrustCenterReferenceInput struct { Description *string `json:"description,omitempty"` WebsiteURL *string `json:"websiteUrl,omitempty"` LogoFile *graphql.Upload `json:"logoFile,omitempty"` + Rank *int `json:"rank,omitempty"` } type UpdateTrustCenterReferencePayload struct { diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go index 297c93343..664e3210a 100644 --- a/pkg/server/api/console/v1/v1_resolver.go +++ b/pkg/server/api/console/v1/v1_resolver.go @@ -1341,7 +1341,7 @@ func (r *mutationResolver) CreateTrustCenterReference(ctx context.Context, input } return &types.CreateTrustCenterReferencePayload{ - TrustCenterReferenceEdge: types.NewTrustCenterReferenceEdge(reference, coredata.TrustCenterReferenceOrderFieldCreatedAt), + TrustCenterReferenceEdge: types.NewTrustCenterReferenceEdge(reference, coredata.TrustCenterReferenceOrderFieldRank), }, nil } @@ -1354,6 +1354,7 @@ func (r *mutationResolver) UpdateTrustCenterReference(ctx context.Context, input Name: input.Name, Description: input.Description, WebsiteURL: input.WebsiteURL, + Rank: input.Rank, } if input.LogoFile != nil { @@ -4944,7 +4945,7 @@ func (r *trustCenterResolver) References(ctx context.Context, obj *types.TrustCe prb := r.ProboService(ctx, obj.ID.TenantID()) pageOrderBy := page.OrderBy[coredata.TrustCenterReferenceOrderField]{ - Field: coredata.TrustCenterReferenceOrderFieldName, + Field: coredata.TrustCenterReferenceOrderFieldRank, Direction: page.OrderDirectionAsc, } if orderBy != nil { diff --git a/pkg/server/api/trust/v1/v1_resolver.go b/pkg/server/api/trust/v1/v1_resolver.go index 0e7c63997..3161f511f 100644 --- a/pkg/server/api/trust/v1/v1_resolver.go +++ b/pkg/server/api/trust/v1/v1_resolver.go @@ -891,8 +891,8 @@ func (r *trustCenterResolver) References(ctx context.Context, obj *types.TrustCe publicTrustService := r.PublicTrustService(ctx, obj.ID.TenantID()) pageOrderBy := page.OrderBy[coredata.TrustCenterReferenceOrderField]{ - Field: coredata.TrustCenterReferenceOrderFieldCreatedAt, - Direction: page.OrderDirectionDesc, + Field: coredata.TrustCenterReferenceOrderFieldRank, + Direction: page.OrderDirectionAsc, } cursor := types.NewCursor(first, after, last, before, pageOrderBy)