Enhance people

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2025-08-12 10:29:13 +02:00
parent 6eb9a39c17
commit d65bc5f7e2
23 changed files with 430 additions and 118 deletions

View File

@@ -38,7 +38,7 @@ function PeopleSelectWithQuery(
) {
const { __ } = useTranslate();
const { name, organizationId, control } = props;
const people = usePeople(organizationId);
const people = usePeople(organizationId, { excludeContractEnded: true });
return (
<>

View File

@@ -16,10 +16,10 @@ import { promisifyMutation, sprintf } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
const peopleQuery = graphql`
query PeopleGraphQuery($organizationId: ID!) {
query PeopleGraphQuery($organizationId: ID!, $filter: PeopleFilter) {
organization: node(id: $organizationId) {
... on Organization {
peoples(first: 100, orderBy: { direction: ASC, field: CREATED_AT }) {
peoples(first: 100, orderBy: { direction: ASC, field: FULL_NAME }, filter: $filter) {
edges {
node {
id
@@ -36,10 +36,15 @@ const peopleQuery = graphql`
/**
* Return a list of people (used for people selectors)
*/
export function usePeople(organizationId: string) {
const data = useLazyLoadQuery<PeopleGraphQuery>(peopleQuery, {
organizationId: organizationId,
});
export function usePeople(organizationId: string, { excludeContractEnded }: { excludeContractEnded?: boolean } = {}) {
const data = useLazyLoadQuery<PeopleGraphQuery>(
peopleQuery,
{
organizationId: organizationId,
filter: excludeContractEnded ? { excludeContractEnded: true } : null,
},
{ fetchPolicy: "network-only" }
);
return useMemo(() => {
return data.organization?.peoples?.edges.map((edge) => edge.node) ?? [];
}, [data]);
@@ -61,7 +66,8 @@ export const paginatedPeopleFragment = graphql`
@refetchable(queryName: "PeopleListQuery")
@argumentDefinitions(
first: { type: "Int", defaultValue: 50 }
order: { type: "PeopleOrder", defaultValue: null }
order: { type: "PeopleOrder", defaultValue: { direction: ASC, field: FULL_NAME } }
filter: { type: "PeopleFilter", defaultValue: null }
after: { type: "CursorKey", defaultValue: null }
before: { type: "CursorKey", defaultValue: null }
last: { type: "Int", defaultValue: null }
@@ -72,6 +78,7 @@ export const paginatedPeopleFragment = graphql`
last: $last
before: $before
orderBy: $order
filter: $filter
) @connection(key: "PeopleGraphPaginatedQuery_peoples") {
__id
edges {
@@ -82,6 +89,8 @@ export const paginatedPeopleFragment = graphql`
kind
position
additionalEmailAddresses
contractStartDate
contractEndDate
}
}
}
@@ -161,6 +170,8 @@ export const peopleNodeQuery = graphql`
position
kind
additionalEmailAddresses
contractStartDate
contractEndDate
}
}
}
@@ -176,6 +187,8 @@ export const updatePeopleMutation = graphql`
position
kind
additionalEmailAddresses
contractStartDate
contractEndDate
}
}
}

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<71df408810680ded020fb0e1cc93ff79>>
* @generated SignedSource<<e263259b92f7b1fe14bca5fca49b20bf>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -16,6 +16,8 @@ export type PeopleGraphNodeQuery$variables = {
export type PeopleGraphNodeQuery$data = {
readonly node: {
readonly additionalEmailAddresses?: ReadonlyArray<string>;
readonly contractEndDate?: any | null | undefined;
readonly contractStartDate?: any | null | undefined;
readonly fullName?: string;
readonly id?: string;
readonly kind?: PeopleKind;
@@ -84,6 +86,20 @@ v7 = {
"kind": "ScalarField",
"name": "additionalEmailAddresses",
"storageKey": null
},
v8 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "contractStartDate",
"storageKey": null
},
v9 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "contractEndDate",
"storageKey": null
};
return {
"fragment": {
@@ -108,7 +124,9 @@ return {
(v4/*: any*/),
(v5/*: any*/),
(v6/*: any*/),
(v7/*: any*/)
(v7/*: any*/),
(v8/*: any*/),
(v9/*: any*/)
],
"type": "People",
"abstractKey": null
@@ -149,7 +167,9 @@ return {
(v4/*: any*/),
(v5/*: any*/),
(v6/*: any*/),
(v7/*: any*/)
(v7/*: any*/),
(v8/*: any*/),
(v9/*: any*/)
],
"type": "People",
"abstractKey": null
@@ -160,16 +180,16 @@ return {
]
},
"params": {
"cacheID": "850de97d42fdda9d2335c0e5219fd067",
"cacheID": "096d011a1e9a7374a13d037738b46fc6",
"id": null,
"metadata": {},
"name": "PeopleGraphNodeQuery",
"operationKind": "query",
"text": "query PeopleGraphNodeQuery(\n $peopleId: ID!\n) {\n node(id: $peopleId) {\n __typename\n ... on People {\n id\n fullName\n primaryEmailAddress\n position\n kind\n additionalEmailAddresses\n }\n id\n }\n}\n"
"text": "query PeopleGraphNodeQuery(\n $peopleId: ID!\n) {\n node(id: $peopleId) {\n __typename\n ... on People {\n id\n fullName\n primaryEmailAddress\n position\n kind\n additionalEmailAddresses\n contractStartDate\n contractEndDate\n }\n id\n }\n}\n"
}
};
})();
(node as any).hash = "f39d45664dba2b3bcdad07ef8baebfe9";
(node as any).hash = "af18ca63be219f85baa39bf99294b4f6";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<5eba326084bfab4966727af60e2aa85c>>
* @generated SignedSource<<940911e0b7ae1ffe53a31f98216dcfe6>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -18,6 +18,8 @@ export type PeopleGraphPaginatedFragment$data = {
readonly edges: ReadonlyArray<{
readonly node: {
readonly additionalEmailAddresses: ReadonlyArray<string>;
readonly contractEndDate: any | null | undefined;
readonly contractStartDate: any | null | undefined;
readonly fullName: string;
readonly id: string;
readonly kind: PeopleKind;
@@ -58,6 +60,11 @@ return {
"kind": "LocalArgument",
"name": "before"
},
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "filter"
},
{
"defaultValue": 50,
"kind": "LocalArgument",
@@ -69,7 +76,10 @@ return {
"name": "last"
},
{
"defaultValue": null,
"defaultValue": {
"direction": "ASC",
"field": "FULL_NAME"
},
"kind": "LocalArgument",
"name": "order"
}
@@ -111,6 +121,11 @@ return {
{
"alias": "peoples",
"args": [
{
"kind": "Variable",
"name": "filter",
"variableName": "filter"
},
{
"kind": "Variable",
"name": "orderBy",
@@ -174,6 +189,20 @@ return {
"name": "additionalEmailAddresses",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "contractStartDate",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "contractEndDate",
"storageKey": null
},
{
"alias": null,
"args": null,
@@ -255,6 +284,6 @@ return {
};
})();
(node as any).hash = "5e095baf6a65e7cdbaa804d4658cbb6b";
(node as any).hash = "c658d1fc05ce8d1c12e6ba4fd3cf8c52";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<1ec11120f71d4bc3040bcdd2665904c0>>
* @generated SignedSource<<c6717878cc2ffa24d0d56aab35c431c7>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -58,6 +58,14 @@ v4 = [
"kind": "Literal",
"name": "first",
"value": 50
},
{
"kind": "Literal",
"name": "orderBy",
"value": {
"direction": "ASC",
"field": "FULL_NAME"
}
}
];
return {
@@ -174,6 +182,20 @@ return {
"name": "additionalEmailAddresses",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "contractStartDate",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "contractEndDate",
"storageKey": null
},
(v3/*: any*/)
],
"storageKey": null
@@ -240,13 +262,14 @@ return {
]
}
],
"storageKey": "peoples(first:50)"
"storageKey": "peoples(first:50,orderBy:{\"direction\":\"ASC\",\"field\":\"FULL_NAME\"})"
},
{
"alias": null,
"args": (v4/*: any*/),
"filters": [
"orderBy"
"orderBy",
"filter"
],
"handle": "connection",
"key": "PeopleGraphPaginatedQuery_peoples",
@@ -263,12 +286,12 @@ return {
]
},
"params": {
"cacheID": "1139d090f54b12e62e08f0a1970c9af0",
"cacheID": "dca6a1202b168afd54cac8484cf6fadd",
"id": null,
"metadata": {},
"name": "PeopleGraphPaginatedQuery",
"operationKind": "query",
"text": "query PeopleGraphPaginatedQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n ...PeopleGraphPaginatedFragment\n }\n id\n }\n}\n\nfragment PeopleGraphPaginatedFragment on Organization {\n peoples(first: 50) {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n kind\n position\n additionalEmailAddresses\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n"
"text": "query PeopleGraphPaginatedQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n ...PeopleGraphPaginatedFragment\n }\n id\n }\n}\n\nfragment PeopleGraphPaginatedFragment on Organization {\n peoples(first: 50, orderBy: {direction: ASC, field: FULL_NAME}) {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n kind\n position\n additionalEmailAddresses\n contractStartDate\n contractEndDate\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n"
}
};
})();

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<fbc31c6da75cbb26f402b2c88664950a>>
* @generated SignedSource<<f8f5d2692a9b278d16d997a3ba952b03>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -9,7 +9,11 @@
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type PeopleFilter = {
excludeContractEnded?: boolean | null | undefined;
};
export type PeopleGraphQuery$variables = {
filter?: PeopleFilter | null | undefined;
organizationId: string;
};
export type PeopleGraphQuery$data = {
@@ -31,33 +35,41 @@ export type PeopleGraphQuery = {
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "organizationId"
}
],
v1 = [
var v0 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "filter"
},
v1 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "organizationId"
},
v2 = [
{
"kind": "Variable",
"name": "id",
"variableName": "organizationId"
}
],
v2 = {
v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v3 = {
v4 = {
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "filter",
"variableName": "filter"
},
{
"kind": "Literal",
"name": "first",
@@ -68,7 +80,7 @@ v3 = {
"name": "orderBy",
"value": {
"direction": "ASC",
"field": "CREATED_AT"
"field": "FULL_NAME"
}
}
],
@@ -93,7 +105,7 @@ v3 = {
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
(v3/*: any*/),
{
"alias": null,
"args": null,
@@ -115,7 +127,7 @@ v3 = {
"storageKey": null
}
],
"storageKey": "peoples(first:100,orderBy:{\"direction\":\"ASC\",\"field\":\"CREATED_AT\"})"
"storageKey": null
}
],
"type": "Organization",
@@ -123,20 +135,23 @@ v3 = {
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"argumentDefinitions": [
(v0/*: any*/),
(v1/*: any*/)
],
"kind": "Fragment",
"metadata": null,
"name": "PeopleGraphQuery",
"selections": [
{
"alias": "organization",
"args": (v1/*: any*/),
"args": (v2/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v3/*: any*/)
(v4/*: any*/)
],
"storageKey": null
}
@@ -146,13 +161,16 @@ return {
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"argumentDefinitions": [
(v1/*: any*/),
(v0/*: any*/)
],
"kind": "Operation",
"name": "PeopleGraphQuery",
"selections": [
{
"alias": "organization",
"args": (v1/*: any*/),
"args": (v2/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
@@ -165,24 +183,24 @@ return {
"name": "__typename",
"storageKey": null
},
(v3/*: any*/),
(v2/*: any*/)
(v4/*: any*/),
(v3/*: any*/)
],
"storageKey": null
}
]
},
"params": {
"cacheID": "cf76244f6aaa0c573bdeb7bf118ad2a3",
"cacheID": "cfb5be60bd39783154a815d178a8fa51",
"id": null,
"metadata": {},
"name": "PeopleGraphQuery",
"operationKind": "query",
"text": "query PeopleGraphQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n peoples(first: 100, orderBy: {direction: ASC, field: CREATED_AT}) {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n }\n }\n }\n }\n id\n }\n}\n"
"text": "query PeopleGraphQuery(\n $organizationId: ID!\n $filter: PeopleFilter\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n peoples(first: 100, orderBy: {direction: ASC, field: FULL_NAME}, filter: $filter) {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n }\n }\n }\n }\n id\n }\n}\n"
}
};
})();
(node as any).hash = "11220f45320400c451abeaf7479a8e47";
(node as any).hash = "79f4d511be3c7cfa63a774ac017a6f41";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<18d62863109c347d5fcf7b5701a9c57b>>
* @generated SignedSource<<a91a49863ad5c692ae869d3bcc0584cb>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -27,6 +27,8 @@ export type PeopleGraphUpdateMutation$data = {
readonly updatePeople: {
readonly people: {
readonly additionalEmailAddresses: ReadonlyArray<string>;
readonly contractEndDate: any | null | undefined;
readonly contractStartDate: any | null | undefined;
readonly fullName: string;
readonly id: string;
readonly kind: PeopleKind;
@@ -112,6 +114,20 @@ v1 = [
"kind": "ScalarField",
"name": "additionalEmailAddresses",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "contractStartDate",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "contractEndDate",
"storageKey": null
}
],
"storageKey": null
@@ -138,16 +154,16 @@ return {
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "814551d1fb24548c41d417342625071f",
"cacheID": "23c51259f3ff117441f9c6e11de5ba5a",
"id": null,
"metadata": {},
"name": "PeopleGraphUpdateMutation",
"operationKind": "mutation",
"text": "mutation PeopleGraphUpdateMutation(\n $input: UpdatePeopleInput!\n) {\n updatePeople(input: $input) {\n people {\n id\n fullName\n primaryEmailAddress\n position\n kind\n additionalEmailAddresses\n }\n }\n}\n"
"text": "mutation PeopleGraphUpdateMutation(\n $input: UpdatePeopleInput!\n) {\n updatePeople(input: $input) {\n people {\n id\n fullName\n primaryEmailAddress\n position\n kind\n additionalEmailAddresses\n contractStartDate\n contractEndDate\n }\n }\n}\n"
}
};
})();
(node as any).hash = "ecc5666046434d5c8e80eaab0e508c11";
(node as any).hash = "fc0946729d6a2729aa1b7c9b2dfbdf7a";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<4cfc7218bc48a818eedc2ea87ac521e6>>
* @generated SignedSource<<e4afda2e1cb4e3fcfccfca64b086ad35>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -12,6 +12,9 @@ import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type OrderDirection = "ASC" | "DESC";
export type PeopleOrderField = "CREATED_AT" | "FULL_NAME" | "KIND";
export type PeopleFilter = {
excludeContractEnded?: boolean | null | undefined;
};
export type PeopleOrder = {
direction: OrderDirection;
field: PeopleOrderField;
@@ -19,6 +22,7 @@ export type PeopleOrder = {
export type PeopleListQuery$variables = {
after?: any | null | undefined;
before?: any | null | undefined;
filter?: PeopleFilter | null | undefined;
first?: number | null | undefined;
id: string;
last?: number | null | undefined;
@@ -46,71 +50,85 @@ v1 = {
"name": "before"
},
v2 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "filter"
},
v3 = {
"defaultValue": 50,
"kind": "LocalArgument",
"name": "first"
},
v3 = {
v4 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "id"
},
v4 = {
v5 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "last"
},
v5 = {
"defaultValue": null,
v6 = {
"defaultValue": {
"direction": "ASC",
"field": "FULL_NAME"
},
"kind": "LocalArgument",
"name": "order"
},
v6 = [
v7 = [
{
"kind": "Variable",
"name": "id",
"variableName": "id"
}
],
v7 = {
v8 = {
"kind": "Variable",
"name": "after",
"variableName": "after"
},
v8 = {
v9 = {
"kind": "Variable",
"name": "before",
"variableName": "before"
},
v9 = {
v10 = {
"kind": "Variable",
"name": "filter",
"variableName": "filter"
},
v11 = {
"kind": "Variable",
"name": "first",
"variableName": "first"
},
v10 = {
v12 = {
"kind": "Variable",
"name": "last",
"variableName": "last"
},
v11 = {
v13 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v12 = {
v14 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v13 = [
(v7/*: any*/),
v15 = [
(v8/*: any*/),
(v9/*: any*/),
(v10/*: any*/),
(v11/*: any*/),
(v12/*: any*/),
{
"kind": "Variable",
"name": "orderBy",
@@ -125,7 +143,8 @@ return {
(v2/*: any*/),
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/)
(v5/*: any*/),
(v6/*: any*/)
],
"kind": "Fragment",
"metadata": null,
@@ -133,7 +152,7 @@ return {
"selections": [
{
"alias": null,
"args": (v6/*: any*/),
"args": (v7/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
@@ -141,10 +160,11 @@ return {
"selections": [
{
"args": [
(v7/*: any*/),
(v8/*: any*/),
(v9/*: any*/),
(v10/*: any*/),
(v11/*: any*/),
(v12/*: any*/),
{
"kind": "Variable",
"name": "order",
@@ -167,29 +187,30 @@ return {
(v0/*: any*/),
(v1/*: any*/),
(v2/*: any*/),
(v4/*: any*/),
(v3/*: any*/),
(v5/*: any*/),
(v3/*: any*/)
(v6/*: any*/),
(v4/*: any*/)
],
"kind": "Operation",
"name": "PeopleListQuery",
"selections": [
{
"alias": null,
"args": (v6/*: any*/),
"args": (v7/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v11/*: any*/),
(v12/*: any*/),
(v13/*: any*/),
(v14/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": (v13/*: any*/),
"args": (v15/*: any*/),
"concreteType": "PeopleConnection",
"kind": "LinkedField",
"name": "peoples",
@@ -211,7 +232,7 @@ return {
"name": "node",
"plural": false,
"selections": [
(v12/*: any*/),
(v14/*: any*/),
{
"alias": null,
"args": null,
@@ -247,7 +268,21 @@ return {
"name": "additionalEmailAddresses",
"storageKey": null
},
(v11/*: any*/)
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "contractStartDate",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "contractEndDate",
"storageKey": null
},
(v13/*: any*/)
],
"storageKey": null
},
@@ -317,9 +352,10 @@ return {
},
{
"alias": null,
"args": (v13/*: any*/),
"args": (v15/*: any*/),
"filters": [
"orderBy"
"orderBy",
"filter"
],
"handle": "connection",
"key": "PeopleGraphPaginatedQuery_peoples",
@@ -336,16 +372,16 @@ return {
]
},
"params": {
"cacheID": "8134caa0eab10c19236a49728f1f77b6",
"cacheID": "7df8a360f24224a73dc98c9b8f6da287",
"id": null,
"metadata": {},
"name": "PeopleListQuery",
"operationKind": "query",
"text": "query PeopleListQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 50\n $last: Int = null\n $order: PeopleOrder = null\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...PeopleGraphPaginatedFragment_16fISc\n id\n }\n}\n\nfragment PeopleGraphPaginatedFragment_16fISc on Organization {\n peoples(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n kind\n position\n additionalEmailAddresses\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n"
"text": "query PeopleListQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $filter: PeopleFilter = null\n $first: Int = 50\n $last: Int = null\n $order: PeopleOrder = {direction: ASC, field: FULL_NAME}\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...PeopleGraphPaginatedFragment_4cFWzS\n id\n }\n}\n\nfragment PeopleGraphPaginatedFragment_4cFWzS on Organization {\n peoples(first: $first, after: $after, last: $last, before: $before, orderBy: $order, filter: $filter) {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n kind\n position\n additionalEmailAddresses\n contractStartDate\n contractEndDate\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n"
}
};
})();
(node as any).hash = "5e095baf6a65e7cdbaa804d4658cbb6b";
(node as any).hash = "c658d1fc05ce8d1c12e6ba4fd3cf8c52";
export default node;

View File

@@ -31,7 +31,7 @@ import { useOrganizationId } from "/hooks/useOrganizationId";
import { ControlledField } from "/components/form/ControlledField";
import { useFormWithSchema } from "/hooks/useFormWithSchema";
import z from "zod";
import { getAuditStateLabel, getAuditStateVariant, auditStates, fileSize, sprintf } from "@probo/helpers";
import { getAuditStateLabel, getAuditStateVariant, auditStates, fileSize, sprintf, formatDatetime } from "@probo/helpers";
import type { AuditGraphNodeQuery } from "/hooks/graph/__generated__/AuditGraphNodeQuery.graphql";
const updateAuditSchema = z.object({
@@ -77,11 +77,6 @@ export default function AuditDetailsPage(props: Props) {
if (!auditEntry.id) return;
try {
const formatDatetime = (dateString?: string) => {
if (!dateString) return undefined;
return `${dateString}T00:00:00Z`;
};
await updateAudit({
id: auditEntry.id,
validFrom: formatDatetime(formData.validFrom),

View File

@@ -16,7 +16,7 @@ import z from "zod";
import { useFormWithSchema } from "/hooks/useFormWithSchema";
import { ControlledField } from "/components/form/ControlledField";
import { useCreateAudit } from "/hooks/graph/AuditGraph";
import { auditStates, getAuditStateLabel } from "@probo/helpers";
import { auditStates, getAuditStateLabel, formatDatetime } from "@probo/helpers";
import { useLazyLoadQuery } from "react-relay";
import { graphql } from "relay-runtime";
import { Suspense } from "react";
@@ -74,12 +74,6 @@ export function CreateAuditDialog({
const onSubmit = handleSubmit(async (data) => {
try {
// Convert date strings to datetime format
const formatDatetime = (dateString?: string) => {
if (!dateString) return undefined;
return `${dateString}T00:00:00Z`;
};
await createAudit({
organizationId,
frameworkId: data.frameworkId,

View File

@@ -26,6 +26,14 @@ import { useOrganizationId } from "/hooks/useOrganizationId";
type People = NodeOf<PeopleGraphPaginatedFragment$data["peoples"]>;
const isContractEnded = (person: People): boolean => {
if (!person.contractEndDate) return false;
const endDate = new Date(person.contractEndDate);
const today = new Date();
today.setHours(0, 0, 0, 0);
return endDate < today;
};
export default function PeopleListPage({
queryRef,
}: {
@@ -87,9 +95,13 @@ function PeopleRow({
const organizationId = useOrganizationId();
const { __ } = useTranslate();
const deletePeople = useDeletePeople(people, connectionId);
const contractEnded = isContractEnded(people);
return (
<Tr to={`/organizations/${organizationId}/people/${people.id}/tasks`}>
<Tr
to={`/organizations/${organizationId}/people/${people.id}/tasks`}
className={contractEnded ? "opacity-50" : ""}
>
<Td>
<div className="flex gap-3 items-center">
<Avatar name={people.fullName} />

View File

@@ -1,5 +1,5 @@
import z from "zod";
import { peopleRoles } from "@probo/helpers";
import { peopleRoles, formatDatetime } from "@probo/helpers";
import { useOutletContext } from "react-router";
import type { PeopleGraphNodeQuery$data } from "/hooks/graph/__generated__/PeopleGraphNodeQuery.graphql";
import { useTranslate } from "@probo/i18n";
@@ -7,7 +7,7 @@ import { useFormWithSchema } from "/hooks/useFormWithSchema";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
import type { PeopleGraphUpdateMutation } from "/hooks/graph/__generated__/PeopleGraphUpdateMutation.graphql";
import { updatePeopleMutation } from "/hooks/graph/PeopleGraph";
import { Button, Card, Field } from "@probo/ui";
import { Button, Card, Field, Input } from "@probo/ui";
import { EmailsField } from "/components/form/EmailsField";
const schema = z.object({
@@ -20,6 +20,8 @@ const schema = z.object({
z.array(z.string().email())
),
kind: z.enum(peopleRoles),
contractStartDate: z.string().optional().nullable(),
contractEndDate: z.string().optional().nullable(),
});
export default function PeopleProfileTab() {
@@ -35,6 +37,8 @@ export default function PeopleProfileTab() {
primaryEmailAddress: people.primaryEmailAddress,
position: people.position,
additionalEmailAddresses: [...(people.additionalEmailAddresses ?? [])],
contractStartDate: people.contractStartDate?.split('T')[0] || "",
contractEndDate: people.contractEndDate?.split('T')[0] || "",
},
});
const [mutate, isMutating] = useMutationWithToasts<PeopleGraphUpdateMutation>(
@@ -46,18 +50,19 @@ export default function PeopleProfileTab() {
);
const onSubmit = handleSubmit((data) => {
const input = {
id: people.id!,
fullName: data.fullName,
primaryEmailAddress: data.primaryEmailAddress,
position: data.position,
additionalEmailAddresses: data.additionalEmailAddresses,
kind: people.kind,
contractStartDate: formatDatetime(data.contractStartDate) ?? null,
contractEndDate: formatDatetime(data.contractEndDate) ?? null,
};
mutate({
variables: {
input: {
id: people.id!,
fullName: data.fullName,
primaryEmailAddress: data.primaryEmailAddress,
position: data.position,
additionalEmailAddresses: data.additionalEmailAddresses,
// TODO : make these field optional in the query (server side)
kind: people.kind,
},
},
variables: { input },
onCompleted: () => {
reset(data);
},
@@ -80,6 +85,12 @@ export default function PeopleProfileTab() {
type="email"
/>
<EmailsField control={control} register={register} />
<Field label={__("Contract start date")}>
<Input {...register("contractStartDate")} type="date" />
</Field>
<Field label={__("Contract end date")}>
<Input {...register("contractEndDate")} type="date" />
</Field>
</Card>
<div className="flex justify-end">
{formState.isDirty && (

View File

@@ -47,6 +47,8 @@ export default function PeopleRoleTab() {
fullName: people.fullName!,
primaryEmailAddress: people.primaryEmailAddress!,
additionalEmailAddresses: people.additionalEmailAddresses ?? [],
contractStartDate: people.contractStartDate,
contractEndDate: people.contractEndDate,
},
},
onCompleted: () => {

View File

@@ -0,0 +1,4 @@
export function formatDatetime(dateString?: string | null): string | undefined {
if (!dateString) return undefined;
return `${dateString}T00:00:00Z`;
}

View File

@@ -18,3 +18,4 @@ export { getAssetTypeVariant, getCriticityVariant } from "./assets";
export { getAuditStateLabel, getAuditStateVariant, auditStates } from "./audits";
export { promisifyMutation } from "./relay";
export { fileType, fileSize } from "./file";
export { formatDatetime } from "./date";

View File

@@ -300,6 +300,7 @@ func (p *Peoples) CountByOrganizationID(
conn pg.Conn,
scope Scoper,
organizationID gid.GID,
filter *PeopleFilter,
) (int, error) {
q := `
SELECT
@@ -309,12 +310,14 @@ FROM
WHERE
%s
AND organization_id = @organization_id
AND %s
`
q = fmt.Sprintf(q, scope.SQLFragment())
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment())
args := pgx.StrictNamedArgs{"organization_id": organizationID}
maps.Copy(args, scope.SQLArguments())
maps.Copy(args, filter.SQLArguments())
row := conn.QueryRow(ctx, q, args)
@@ -333,6 +336,7 @@ func (p *Peoples) LoadByOrganizationID(
scope Scoper,
organizationID gid.GID,
cursor *page.Cursor[PeopleOrderField],
filter *PeopleFilter,
) error {
q := `
SELECT
@@ -354,13 +358,15 @@ WHERE
%s
AND organization_id = @organization_id
AND %s
AND %s
`
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
args := pgx.StrictNamedArgs{"organization_id": organizationID}
maps.Copy(args, cursor.SQLArguments())
maps.Copy(args, scope.SQLArguments())
maps.Copy(args, filter.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {

View File

@@ -0,0 +1,53 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package coredata
import (
"time"
"github.com/jackc/pgx/v5"
)
type (
PeopleFilter struct {
excludeContractEnded *bool
currentDate time.Time
}
)
func NewPeopleFilter(excludeContractEnded *bool) *PeopleFilter {
return &PeopleFilter{
excludeContractEnded: excludeContractEnded,
currentDate: time.Now(),
}
}
func (f *PeopleFilter) SQLArguments() pgx.StrictNamedArgs {
return pgx.StrictNamedArgs{
"exclude_contract_ended": f.excludeContractEnded,
"current_date": f.currentDate,
}
}
func (f *PeopleFilter) SQLFragment() string {
return `
(
CASE
WHEN @exclude_contract_ended::boolean IS NOT NULL AND @exclude_contract_ended::boolean = true THEN
(contract_end_date IS NULL OR contract_end_date >= @current_date::date)
ELSE TRUE
END
)`
}

View File

@@ -98,6 +98,7 @@ func (s PeopleService) GetByUserID(
func (s PeopleService) CountForOrganizationID(
ctx context.Context,
organizationID gid.GID,
filter *coredata.PeopleFilter,
) (int, error) {
var count int
@@ -105,7 +106,7 @@ func (s PeopleService) CountForOrganizationID(
ctx,
func(conn pg.Conn) (err error) {
peoples := coredata.Peoples{}
count, err = peoples.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID)
count, err = peoples.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID, filter)
if err != nil {
return fmt.Errorf("cannot count peoples: %w", err)
}
@@ -125,6 +126,7 @@ func (s PeopleService) ListForOrganizationID(
ctx context.Context,
organizationID gid.GID,
cursor *page.Cursor[coredata.PeopleOrderField],
filter *coredata.PeopleFilter,
) (*page.Page[*coredata.People, coredata.PeopleOrderField], error) {
var peoples coredata.Peoples
@@ -137,6 +139,7 @@ func (s PeopleService) ListForOrganizationID(
s.svc.scope,
organizationID,
cursor,
filter,
)
},
)

View File

@@ -718,6 +718,10 @@ input RiskFilter {
query: String
}
input PeopleFilter {
excludeContractEnded: Boolean
}
input OrganizationFilter {
trustCenterSlug: String
}
@@ -796,6 +800,7 @@ type Organization implements Node {
last: Int
before: CursorKey
orderBy: PeopleOrder
filter: PeopleFilter
): PeopleConnection! @goField(forceResolver: true)
documents(

View File

@@ -665,7 +665,7 @@ type ComplexityRoot struct {
LogoURL func(childComplexity int) int
Measures func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.MeasureOrderBy, filter *types.MeasureFilter) int
Name func(childComplexity int) int
Peoples func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.PeopleOrderBy) int
Peoples func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.PeopleOrderBy, filter *types.PeopleFilter) int
Risks func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.RiskOrderBy, filter *types.RiskFilter) int
Tasks func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.TaskOrderBy) int
TrustCenter func(childComplexity int) int
@@ -1212,7 +1212,7 @@ type OrganizationResolver interface {
Frameworks(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.FrameworkOrderBy) (*types.FrameworkConnection, error)
Controls(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ControlOrderBy, filter *types.ControlFilter) (*types.ControlConnection, error)
Vendors(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.VendorOrderBy) (*types.VendorConnection, error)
Peoples(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.PeopleOrderBy) (*types.PeopleConnection, error)
Peoples(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.PeopleOrderBy, filter *types.PeopleFilter) (*types.PeopleConnection, error)
Documents(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.DocumentOrderBy, filter *types.DocumentFilter) (*types.DocumentConnection, error)
Measures(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.MeasureOrderBy, filter *types.MeasureFilter) (*types.MeasureConnection, error)
Risks(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.RiskOrderBy, filter *types.RiskFilter) (*types.RiskConnection, error)
@@ -4009,7 +4009,7 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return 0, false
}
return e.complexity.Organization.Peoples(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey), args["orderBy"].(*types.PeopleOrderBy)), true
return e.complexity.Organization.Peoples(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey), args["orderBy"].(*types.PeopleOrderBy), args["filter"].(*types.PeopleFilter)), true
case "Organization.risks":
if e.complexity.Organization.Risks == nil {
@@ -5551,6 +5551,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
ec.unmarshalInputMeasureOrder,
ec.unmarshalInputOrganizationFilter,
ec.unmarshalInputOrganizationOrder,
ec.unmarshalInputPeopleFilter,
ec.unmarshalInputPeopleOrder,
ec.unmarshalInputPublishDocumentVersionInput,
ec.unmarshalInputRemoveUserInput,
@@ -6402,6 +6403,10 @@ input RiskFilter {
query: String
}
input PeopleFilter {
excludeContractEnded: Boolean
}
input OrganizationFilter {
trustCenterSlug: String
}
@@ -6480,6 +6485,7 @@ type Organization implements Node {
last: Int
before: CursorKey
orderBy: PeopleOrder
filter: PeopleFilter
): PeopleConnection! @goField(forceResolver: true)
documents(
@@ -12229,6 +12235,11 @@ func (ec *executionContext) field_Organization_peoples_args(ctx context.Context,
return nil, err
}
args["orderBy"] = arg4
arg5, err := ec.field_Organization_peoples_argsFilter(ctx, rawArgs)
if err != nil {
return nil, err
}
args["filter"] = arg5
return args, nil
}
func (ec *executionContext) field_Organization_peoples_argsFirst(
@@ -12296,6 +12307,19 @@ func (ec *executionContext) field_Organization_peoples_argsOrderBy(
return zeroVal, nil
}
func (ec *executionContext) field_Organization_peoples_argsFilter(
ctx context.Context,
rawArgs map[string]any,
) (*types.PeopleFilter, error) {
ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("filter"))
if tmp, ok := rawArgs["filter"]; ok {
return ec.unmarshalOPeopleFilter2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐPeopleFilter(ctx, tmp)
}
var zeroVal *types.PeopleFilter
return zeroVal, nil
}
func (ec *executionContext) field_Organization_risks_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error
args := map[string]any{}
@@ -30201,7 +30225,7 @@ func (ec *executionContext) _Organization_peoples(ctx context.Context, field gra
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Organization().Peoples(rctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*page.CursorKey), fc.Args["last"].(*int), fc.Args["before"].(*page.CursorKey), fc.Args["orderBy"].(*types.PeopleOrderBy))
return ec.resolvers.Organization().Peoples(rctx, obj, fc.Args["first"].(*int), fc.Args["after"].(*page.CursorKey), fc.Args["last"].(*int), fc.Args["before"].(*page.CursorKey), fc.Args["orderBy"].(*types.PeopleOrderBy), fc.Args["filter"].(*types.PeopleFilter))
})
if err != nil {
ec.Error(ctx, err)
@@ -45554,6 +45578,33 @@ func (ec *executionContext) unmarshalInputOrganizationOrder(ctx context.Context,
return it, nil
}
func (ec *executionContext) unmarshalInputPeopleFilter(ctx context.Context, obj any) (types.PeopleFilter, error) {
var it types.PeopleFilter
asMap := map[string]any{}
for k, v := range obj.(map[string]any) {
asMap[k] = v
}
fieldsInOrder := [...]string{"excludeContractEnded"}
for _, k := range fieldsInOrder {
v, ok := asMap[k]
if !ok {
continue
}
switch k {
case "excludeContractEnded":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("excludeContractEnded"))
data, err := ec.unmarshalOBoolean2ᚖbool(ctx, v)
if err != nil {
return it, err
}
it.ExcludeContractEnded = data
}
}
return it, nil
}
func (ec *executionContext) unmarshalInputPeopleOrder(ctx context.Context, obj any) (types.PeopleOrderBy, error) {
var it types.PeopleOrderBy
asMap := map[string]any{}
@@ -63240,6 +63291,14 @@ func (ec *executionContext) marshalOPeople2ᚖgithubᚗcomᚋgetproboᚋproboᚋ
return ec._People(ctx, sel, v)
}
func (ec *executionContext) unmarshalOPeopleFilter2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐPeopleFilter(ctx context.Context, v any) (*types.PeopleFilter, error) {
if v == nil {
return nil, nil
}
res, err := ec.unmarshalInputPeopleFilter(ctx, v)
return &res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) unmarshalOPeopleKind2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐPeopleKind(ctx context.Context, v any) (*coredata.PeopleKind, error) {
if v == nil {
return nil, nil

View File

@@ -30,6 +30,7 @@ type (
Resolver any
ParentID gid.GID
Filters *coredata.PeopleFilter
}
)
@@ -37,6 +38,7 @@ func NewPeopleConnection(
p *page.Page[*coredata.People, coredata.PeopleOrderField],
parentType any,
parentID gid.GID,
filters *coredata.PeopleFilter,
) *PeopleConnection {
var edges = make([]*PeopleEdge, len(p.Data))
@@ -50,6 +52,7 @@ func NewPeopleConnection(
Resolver: parentType,
ParentID: parentID,
Filters: filters,
}
}

View File

@@ -907,6 +907,10 @@ type PeopleEdge struct {
Node *People `json:"node"`
}
type PeopleFilter struct {
ExcludeContractEnded *bool `json:"excludeContractEnded,omitempty"`
}
type PublishDocumentVersionInput struct {
DocumentID gid.GID `json:"documentId"`
Changelog *string `json:"changelog,omitempty"`

View File

@@ -2587,7 +2587,7 @@ func (r *organizationResolver) Vendors(ctx context.Context, obj *types.Organizat
}
// Peoples is the resolver for the peoples field.
func (r *organizationResolver) Peoples(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.PeopleOrderBy) (*types.PeopleConnection, error) {
func (r *organizationResolver) Peoples(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.PeopleOrderBy, filter *types.PeopleFilter) (*types.PeopleConnection, error) {
prb := r.ProboService(ctx, obj.ID.TenantID())
pageOrderBy := page.OrderBy[coredata.PeopleOrderField]{
@@ -2603,12 +2603,17 @@ func (r *organizationResolver) Peoples(ctx context.Context, obj *types.Organizat
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
page, err := prb.Peoples.ListForOrganizationID(ctx, obj.ID, cursor)
var peopleFilter = coredata.NewPeopleFilter(nil)
if filter != nil {
peopleFilter = coredata.NewPeopleFilter(filter.ExcludeContractEnded)
}
page, err := prb.Peoples.ListForOrganizationID(ctx, obj.ID, cursor, peopleFilter)
if err != nil {
panic(fmt.Errorf("cannot list organization peoples: %w", err))
}
return types.NewPeopleConnection(page, r, obj.ID), nil
return types.NewPeopleConnection(page, r, obj.ID, peopleFilter), nil
}
// Documents is the resolver for the documents field.
@@ -2819,7 +2824,7 @@ func (r *peopleConnectionResolver) TotalCount(ctx context.Context, obj *types.Pe
switch obj.Resolver.(type) {
case *organizationResolver:
count, err := prb.Peoples.CountForOrganizationID(ctx, obj.ParentID)
count, err := prb.Peoples.CountForOrganizationID(ctx, obj.ParentID, obj.Filters)
if err != nil {
return 0, fmt.Errorf("cannot count peoples: %w", err)
}