Add rank to reference

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2025-10-28 16:38:12 +01:00
parent ca008721ba
commit 75f3f92f04
17 changed files with 497 additions and 50 deletions

View File

@@ -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<typeof referenceSchema>;
@@ -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<TrustCenterReferenceDialogRef, { children?: ReactNode }>(
@@ -89,6 +92,7 @@ export const TrustCenterReferenceDialog = forwardRef<TrustCenterReferenceDialogR
name: reference.name,
description: reference.description,
websiteUrl: reference.websiteUrl,
rank: reference.rank,
});
dialogRef.current?.open();
},
@@ -133,6 +137,7 @@ export const TrustCenterReferenceDialog = forwardRef<TrustCenterReferenceDialogR
name: string;
description: string;
websiteUrl: string;
rank?: number;
logoFile?: null;
} = {
id: editReference.id,
@@ -141,6 +146,10 @@ export const TrustCenterReferenceDialog = forwardRef<TrustCenterReferenceDialogR
websiteUrl: data.websiteUrl,
};
if (data.rank !== undefined) {
input.rank = data.rank;
}
const uploadables: Record<string, File> = {};
if (uploadedFile) {
@@ -210,6 +219,18 @@ export const TrustCenterReferenceDialog = forwardRef<TrustCenterReferenceDialogR
placeholder={__("https://example.com")}
/>
{mode === 'edit' && (
<Field
{...register("rank", { valueAsNumber: true })}
label={__("Rank")}
type="number"
min={1}
error={errors.rank?.message}
placeholder={__("Display order (1, 2, 3...)")}
help={__("Lower numbers appear first")}
/>
)}
<Field label={__("Logo")}>
<Dropzone
description={__("Upload logo image (PNG, JPG, WEBP up to 5MB)")}

View File

@@ -14,9 +14,10 @@ import {
IconPencil,
IconArrowLink,
} from "@probo/ui";
import { type ReactNode, useRef } from "react";
import { type ReactNode, useRef, useState } from "react";
import {
useTrustCenterReferences,
useUpdateTrustCenterReferenceRankMutation,
} from "/hooks/graph/TrustCenterReferenceGraph";
import { TrustCenterReferenceDialog, type TrustCenterReferenceDialogRef } from "./TrustCenterReferenceDialog";
import { DeleteTrustCenterReferenceDialog } from "./DeleteTrustCenterReferenceDialog";
@@ -32,6 +33,7 @@ type Reference = {
description: string;
websiteUrl: string;
logoUrl: string;
rank: number;
createdAt: string;
updatedAt: string;
};
@@ -39,7 +41,11 @@ type Reference = {
export function TrustCenterReferencesSection({ trustCenterId }: Props) {
const { __ } = useTranslate();
const dialogRef = useRef<TrustCenterReferenceDialogRef>(null);
const data = useTrustCenterReferences(trustCenterId);
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
const [dragOverIndex, setDragOverIndex] = useState<number | null>(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 (
<div className="space-y-4">
<div className="flex items-center justify-between">
@@ -93,18 +135,28 @@ export function TrustCenterReferencesSection({ trustCenterId }: Props) {
</Td>
</Tr>
)}
{references.map((reference: Reference) => (
{references.map((reference: Reference, index: number) => (
<ReferenceRow
key={reference.id}
reference={reference}
index={index}
isDragging={draggedIndex === index}
isDropTarget={dragOverIndex === index && draggedIndex !== index}
onEdit={() => handleEdit(reference)}
connectionId={referencesConnectionId}
onVisitWebsite={() => handleVisitWebsite(reference.websiteUrl)}
onDragStart={() => handleDragStart(index)}
onDragOver={(e) => handleDragOver(e, index)}
onDrop={() => handleDrop(index)}
/>
))}
</Tbody>
</Table>
<p className="text-xs text-txt-tertiary">
{__("Drag and drop references to change their displayed order")}
</p>
<TrustCenterReferenceDialog ref={dialogRef} />
</div>
);
@@ -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 (
<Tr>
<Tr
draggable
onDragStart={onDragStart}
onDragOver={onDragOver}
onDrop={onDrop}
onMouseDown={() => setIsMouseDown(true)}
onMouseUp={() => setIsMouseDown(false)}
onMouseLeave={() => setIsMouseDown(false)}
className={className}
>
<Td>
<div className="flex items-center gap-3">
<Avatar

View File

@@ -7,6 +7,7 @@ import type {
} from "./__generated__/TrustCenterReferenceGraphQuery.graphql";
import type { TrustCenterReferenceGraphCreateMutation } from "./__generated__/TrustCenterReferenceGraphCreateMutation.graphql";
import type { TrustCenterReferenceGraphUpdateMutation } from "./__generated__/TrustCenterReferenceGraphUpdateMutation.graphql";
import type { TrustCenterReferenceGraphUpdateRankMutation } from "./__generated__/TrustCenterReferenceGraphUpdateRankMutation.graphql";
import type { TrustCenterReferenceGraphDeleteMutation } from "./__generated__/TrustCenterReferenceGraphDeleteMutation.graphql";
export const trustCenterReferencesQuery = graphql`
@@ -14,7 +15,7 @@ export const trustCenterReferencesQuery = graphql`
node(id: $trustCenterId) {
... on TrustCenter {
id
references(first: 100, orderBy: { field: CREATED_AT, direction: DESC })
references(first: 100, orderBy: { field: RANK, direction: ASC })
@connection(key: "TrustCenterReferencesSection_references") {
__id
pageInfo {
@@ -31,6 +32,7 @@ export const trustCenterReferencesQuery = graphql`
description
websiteUrl
logoUrl
rank
createdAt
updatedAt
}
@@ -47,7 +49,7 @@ export const createTrustCenterReferenceMutation = graphql`
$connections: [ID!]!
) {
createTrustCenterReference(input: $input) {
trustCenterReferenceEdge @prependEdge(connections: $connections) {
trustCenterReferenceEdge @appendEdge(connections: $connections) {
cursor
node {
id
@@ -55,6 +57,7 @@ export const createTrustCenterReferenceMutation = graphql`
description
websiteUrl
logoUrl
rank
createdAt
updatedAt
}
@@ -74,6 +77,7 @@ export const updateTrustCenterReferenceMutation = graphql`
description
websiteUrl
logoUrl
rank
createdAt
updatedAt
}
@@ -92,11 +96,11 @@ export const deleteTrustCenterReferenceMutation = graphql`
}
`;
export function useTrustCenterReferences(trustCenterId: string): TrustCenterReferenceGraphQuery$data | null {
export function useTrustCenterReferences(trustCenterId: string, refetchKey = 0): TrustCenterReferenceGraphQuery$data | null {
const data = useLazyLoadQuery<TrustCenterReferenceGraphQuery>(
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<TrustCenterReferenceGraphUpdateRankMutation>(
updateTrustCenterReferenceRankMutation,
{
successMessage: "Order updated successfully",
errorMessage: "Failed to update order",
}
);
}
export function useDeleteTrustCenterReferenceMutation() {
return useMutationWithToasts<TrustCenterReferenceGraphDeleteMutation>(
deleteTrustCenterReferenceMutation,

View File

@@ -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;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<f53f001d217e4c6f5b353f2243f700f2>>
* @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;

View File

@@ -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;

View File

@@ -0,0 +1,118 @@
/**
* @generated SignedSource<<ef22c5c717241729685db94c9e9c7701>>
* @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;