Refactor invitation system
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -45,14 +45,15 @@ const schema = z.object({
|
||||
|
||||
type Props = PropsWithChildren & {
|
||||
connectionId?: string;
|
||||
onRefetch: () => void;
|
||||
};
|
||||
|
||||
export function InviteUserDialog({ children, connectionId }: Props) {
|
||||
export function InviteUserDialog({ children, connectionId, onRefetch }: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const organizationId = useOrganizationId();
|
||||
const [inviteUser, isInviting] = useMutationWithToasts(inviteMutation, {
|
||||
successMessage: __("User invited successfully"),
|
||||
errorMessage: __("Failed to invite user"),
|
||||
successMessage: __("Invitation sent successfully"),
|
||||
errorMessage: __("Failed to send invitation"),
|
||||
});
|
||||
const { register, handleSubmit, formState, reset, control } = useFormWithSchema(
|
||||
schema,
|
||||
@@ -72,9 +73,10 @@ export function InviteUserDialog({ children, connectionId }: Props) {
|
||||
},
|
||||
connections: connectionId ? [connectionId] : ["SettingsPageInvitations_invitations"],
|
||||
},
|
||||
onSuccess: () => {
|
||||
onCompleted: () => {
|
||||
reset();
|
||||
dialogRef.current?.close();
|
||||
onRefetch();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -147,7 +147,7 @@ export const paginatedVendorsFragment = graphql`
|
||||
`;
|
||||
|
||||
export const vendorNodeQuery = graphql`
|
||||
query VendorGraphNodeQuery($vendorId: ID!, $organizationId: ID!) {
|
||||
query VendorGraphNodeQuery($vendorId: ID!) {
|
||||
node(id: $vendorId) {
|
||||
... on Vendor {
|
||||
id
|
||||
@@ -166,9 +166,6 @@ export const vendorNodeQuery = graphql`
|
||||
viewer {
|
||||
user {
|
||||
id
|
||||
people(organizationId: $organizationId) {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<fdfadfbc4f5fdecf5f70a8c92d3ded88>>
|
||||
* @generated SignedSource<<cd9f55de7564e6b9e142c91a79bd9d9d>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -11,7 +11,6 @@
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
import { FragmentRefs } from "relay-runtime";
|
||||
export type VendorGraphNodeQuery$variables = {
|
||||
organizationId: string;
|
||||
vendorId: string;
|
||||
};
|
||||
export type VendorGraphNodeQuery$data = {
|
||||
@@ -25,9 +24,6 @@ export type VendorGraphNodeQuery$data = {
|
||||
readonly viewer: {
|
||||
readonly user: {
|
||||
readonly id: string;
|
||||
readonly people: {
|
||||
readonly id: string;
|
||||
} | null | undefined;
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -37,153 +33,132 @@ export type VendorGraphNodeQuery = {
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
var v0 = {
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "organizationId"
|
||||
},
|
||||
v1 = {
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "vendorId"
|
||||
},
|
||||
v2 = [
|
||||
var v0 = [
|
||||
{
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "vendorId"
|
||||
}
|
||||
],
|
||||
v1 = [
|
||||
{
|
||||
"kind": "Variable",
|
||||
"name": "id",
|
||||
"variableName": "vendorId"
|
||||
}
|
||||
],
|
||||
v3 = {
|
||||
v2 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "id",
|
||||
"storageKey": null
|
||||
},
|
||||
v4 = {
|
||||
v3 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "snapshotId",
|
||||
"storageKey": null
|
||||
},
|
||||
v5 = {
|
||||
v4 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "name",
|
||||
"storageKey": null
|
||||
},
|
||||
v6 = {
|
||||
v5 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "websiteUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
v7 = [
|
||||
(v3/*: any*/)
|
||||
v6 = [
|
||||
(v2/*: any*/)
|
||||
],
|
||||
v8 = {
|
||||
v7 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "User",
|
||||
"kind": "LinkedField",
|
||||
"name": "user",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v3/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": [
|
||||
{
|
||||
"kind": "Variable",
|
||||
"name": "organizationId",
|
||||
"variableName": "organizationId"
|
||||
}
|
||||
],
|
||||
"concreteType": "People",
|
||||
"kind": "LinkedField",
|
||||
"name": "people",
|
||||
"plural": false,
|
||||
"selections": (v7/*: any*/),
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"selections": (v6/*: any*/),
|
||||
"storageKey": null
|
||||
},
|
||||
v9 = {
|
||||
v8 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "__typename",
|
||||
"storageKey": null
|
||||
},
|
||||
v10 = {
|
||||
v9 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "description",
|
||||
"storageKey": null
|
||||
},
|
||||
v11 = [
|
||||
v10 = [
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "first",
|
||||
"value": 50
|
||||
}
|
||||
],
|
||||
v12 = {
|
||||
v11 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "validUntil",
|
||||
"storageKey": null
|
||||
},
|
||||
v13 = {
|
||||
v12 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "fileName",
|
||||
"storageKey": null
|
||||
},
|
||||
v14 = {
|
||||
v13 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "cursor",
|
||||
"storageKey": null
|
||||
},
|
||||
v15 = {
|
||||
v14 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "endCursor",
|
||||
"storageKey": null
|
||||
},
|
||||
v16 = {
|
||||
v15 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "hasNextPage",
|
||||
"storageKey": null
|
||||
},
|
||||
v17 = {
|
||||
v16 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "hasPreviousPage",
|
||||
"storageKey": null
|
||||
},
|
||||
v18 = {
|
||||
v17 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "startCursor",
|
||||
"storageKey": null
|
||||
},
|
||||
v19 = {
|
||||
v18 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "PageInfo",
|
||||
@@ -191,14 +166,14 @@ v19 = {
|
||||
"name": "pageInfo",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v14/*: any*/),
|
||||
(v15/*: any*/),
|
||||
(v16/*: any*/),
|
||||
(v17/*: any*/),
|
||||
(v18/*: any*/)
|
||||
(v17/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
v20 = {
|
||||
v19 = {
|
||||
"kind": "ClientExtension",
|
||||
"selections": [
|
||||
{
|
||||
@@ -210,26 +185,26 @@ v20 = {
|
||||
}
|
||||
]
|
||||
},
|
||||
v21 = [
|
||||
v20 = [
|
||||
"orderBy"
|
||||
],
|
||||
v22 = {
|
||||
v21 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "createdAt",
|
||||
"storageKey": null
|
||||
},
|
||||
v23 = {
|
||||
v22 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "updatedAt",
|
||||
"storageKey": null
|
||||
},
|
||||
v24 = [
|
||||
(v3/*: any*/),
|
||||
(v13/*: any*/),
|
||||
v23 = [
|
||||
(v2/*: any*/),
|
||||
(v12/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
@@ -244,22 +219,19 @@ v24 = [
|
||||
"name": "validFrom",
|
||||
"storageKey": null
|
||||
},
|
||||
(v12/*: any*/),
|
||||
(v22/*: any*/)
|
||||
(v11/*: any*/),
|
||||
(v21/*: any*/)
|
||||
];
|
||||
return {
|
||||
"fragment": {
|
||||
"argumentDefinitions": [
|
||||
(v0/*: any*/),
|
||||
(v1/*: any*/)
|
||||
],
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "VendorGraphNodeQuery",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v2/*: any*/),
|
||||
"args": (v1/*: any*/),
|
||||
"concreteType": null,
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
@@ -268,10 +240,10 @@ return {
|
||||
{
|
||||
"kind": "InlineFragment",
|
||||
"selections": [
|
||||
(v2/*: any*/),
|
||||
(v3/*: any*/),
|
||||
(v4/*: any*/),
|
||||
(v5/*: any*/),
|
||||
(v6/*: any*/),
|
||||
{
|
||||
"args": null,
|
||||
"kind": "FragmentSpread",
|
||||
@@ -322,7 +294,7 @@ return {
|
||||
"name": "viewer",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v8/*: any*/)
|
||||
(v7/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
@@ -332,30 +304,27 @@ return {
|
||||
},
|
||||
"kind": "Request",
|
||||
"operation": {
|
||||
"argumentDefinitions": [
|
||||
(v1/*: any*/),
|
||||
(v0/*: any*/)
|
||||
],
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Operation",
|
||||
"name": "VendorGraphNodeQuery",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v2/*: any*/),
|
||||
"args": (v1/*: any*/),
|
||||
"concreteType": null,
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v9/*: any*/),
|
||||
(v3/*: any*/),
|
||||
(v8/*: any*/),
|
||||
(v2/*: any*/),
|
||||
{
|
||||
"kind": "InlineFragment",
|
||||
"selections": [
|
||||
(v3/*: any*/),
|
||||
(v4/*: any*/),
|
||||
(v5/*: any*/),
|
||||
(v6/*: any*/),
|
||||
(v10/*: any*/),
|
||||
(v9/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
@@ -447,7 +416,7 @@ return {
|
||||
"kind": "LinkedField",
|
||||
"name": "businessOwner",
|
||||
"plural": false,
|
||||
"selections": (v7/*: any*/),
|
||||
"selections": (v6/*: any*/),
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
@@ -457,12 +426,12 @@ return {
|
||||
"kind": "LinkedField",
|
||||
"name": "securityOwner",
|
||||
"plural": false,
|
||||
"selections": (v7/*: any*/),
|
||||
"selections": (v6/*: any*/),
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v11/*: any*/),
|
||||
"args": (v10/*: any*/),
|
||||
"concreteType": "VendorComplianceReportConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "complianceReports",
|
||||
@@ -484,7 +453,7 @@ return {
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v3/*: any*/),
|
||||
(v2/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
@@ -492,7 +461,7 @@ return {
|
||||
"name": "reportDate",
|
||||
"storageKey": null
|
||||
},
|
||||
(v12/*: any*/),
|
||||
(v11/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
@@ -508,7 +477,7 @@ return {
|
||||
"name": "file",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v13/*: any*/),
|
||||
(v12/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
@@ -523,27 +492,27 @@ return {
|
||||
"name": "size",
|
||||
"storageKey": null
|
||||
},
|
||||
(v3/*: any*/)
|
||||
(v2/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
(v9/*: any*/)
|
||||
(v8/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
(v14/*: any*/)
|
||||
(v13/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
(v19/*: any*/),
|
||||
(v20/*: any*/)
|
||||
(v18/*: any*/),
|
||||
(v19/*: any*/)
|
||||
],
|
||||
"storageKey": "complianceReports(first:50)"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v11/*: any*/),
|
||||
"filters": (v21/*: any*/),
|
||||
"args": (v10/*: any*/),
|
||||
"filters": (v20/*: any*/),
|
||||
"handle": "connection",
|
||||
"key": "VendorComplianceTabFragment_complianceReports",
|
||||
"kind": "LinkedHandle",
|
||||
@@ -551,7 +520,7 @@ return {
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v11/*: any*/),
|
||||
"args": (v10/*: any*/),
|
||||
"concreteType": "VendorContactConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "contacts",
|
||||
@@ -573,7 +542,7 @@ return {
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v3/*: any*/),
|
||||
(v2/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
@@ -602,25 +571,25 @@ return {
|
||||
"name": "role",
|
||||
"storageKey": null
|
||||
},
|
||||
(v21/*: any*/),
|
||||
(v22/*: any*/),
|
||||
(v23/*: any*/),
|
||||
(v9/*: any*/)
|
||||
(v8/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
(v14/*: any*/)
|
||||
(v13/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
(v19/*: any*/),
|
||||
(v20/*: any*/)
|
||||
(v18/*: any*/),
|
||||
(v19/*: any*/)
|
||||
],
|
||||
"storageKey": "contacts(first:50)"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v11/*: any*/),
|
||||
"filters": (v21/*: any*/),
|
||||
"args": (v10/*: any*/),
|
||||
"filters": (v20/*: any*/),
|
||||
"handle": "connection",
|
||||
"key": "VendorContactsTabFragment_contacts",
|
||||
"kind": "LinkedHandle",
|
||||
@@ -628,7 +597,7 @@ return {
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v11/*: any*/),
|
||||
"args": (v10/*: any*/),
|
||||
"concreteType": "VendorServiceConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "services",
|
||||
@@ -650,28 +619,28 @@ return {
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v3/*: any*/),
|
||||
(v5/*: any*/),
|
||||
(v10/*: any*/),
|
||||
(v2/*: any*/),
|
||||
(v4/*: any*/),
|
||||
(v9/*: any*/),
|
||||
(v21/*: any*/),
|
||||
(v22/*: any*/),
|
||||
(v23/*: any*/),
|
||||
(v9/*: any*/)
|
||||
(v8/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
(v14/*: any*/)
|
||||
(v13/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
(v19/*: any*/),
|
||||
(v20/*: any*/)
|
||||
(v18/*: any*/),
|
||||
(v19/*: any*/)
|
||||
],
|
||||
"storageKey": "services(first:50)"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v11/*: any*/),
|
||||
"filters": (v21/*: any*/),
|
||||
"args": (v10/*: any*/),
|
||||
"filters": (v20/*: any*/),
|
||||
"handle": "connection",
|
||||
"key": "VendorServicesTabFragment_services",
|
||||
"kind": "LinkedHandle",
|
||||
@@ -679,7 +648,7 @@ return {
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v11/*: any*/),
|
||||
"args": (v10/*: any*/),
|
||||
"concreteType": "VendorRiskAssessmentConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "riskAssessments",
|
||||
@@ -701,8 +670,8 @@ return {
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v3/*: any*/),
|
||||
(v22/*: any*/),
|
||||
(v2/*: any*/),
|
||||
(v21/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
@@ -731,11 +700,11 @@ return {
|
||||
"name": "notes",
|
||||
"storageKey": null
|
||||
},
|
||||
(v9/*: any*/)
|
||||
(v8/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
(v14/*: any*/)
|
||||
(v13/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
@@ -747,21 +716,21 @@ return {
|
||||
"name": "pageInfo",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v16/*: any*/),
|
||||
(v15/*: any*/),
|
||||
(v17/*: any*/),
|
||||
(v18/*: any*/)
|
||||
(v14/*: any*/),
|
||||
(v16/*: any*/),
|
||||
(v17/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
(v20/*: any*/)
|
||||
(v19/*: any*/)
|
||||
],
|
||||
"storageKey": "riskAssessments(first:50)"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v11/*: any*/),
|
||||
"filters": (v21/*: any*/),
|
||||
"args": (v10/*: any*/),
|
||||
"filters": (v20/*: any*/),
|
||||
"handle": "connection",
|
||||
"key": "VendorRiskAssessmentTabFragment_riskAssessments",
|
||||
"kind": "LinkedHandle",
|
||||
@@ -774,7 +743,7 @@ return {
|
||||
"kind": "LinkedField",
|
||||
"name": "businessAssociateAgreement",
|
||||
"plural": false,
|
||||
"selections": (v24/*: any*/),
|
||||
"selections": (v23/*: any*/),
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
@@ -784,7 +753,7 @@ return {
|
||||
"kind": "LinkedField",
|
||||
"name": "dataPrivacyAgreement",
|
||||
"plural": false,
|
||||
"selections": (v24/*: any*/),
|
||||
"selections": (v23/*: any*/),
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
@@ -802,24 +771,24 @@ return {
|
||||
"name": "viewer",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v8/*: any*/),
|
||||
(v3/*: any*/)
|
||||
(v7/*: any*/),
|
||||
(v2/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "faee4f059ef9baab46d5f2420e36bb60",
|
||||
"cacheID": "b4e4f090951f025be6f359df3d2dc41a",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "VendorGraphNodeQuery",
|
||||
"operationKind": "query",
|
||||
"text": "query VendorGraphNodeQuery(\n $vendorId: ID!\n $organizationId: ID!\n) {\n node(id: $vendorId) {\n __typename\n ... on Vendor {\n id\n snapshotId\n name\n websiteUrl\n ...useVendorFormFragment\n ...VendorComplianceTabFragment\n ...VendorContactsTabFragment\n ...VendorServicesTabFragment\n ...VendorRiskAssessmentTabFragment\n ...VendorOverviewTabBusinessAssociateAgreementFragment\n ...VendorOverviewTabDataPrivacyAgreementFragment\n }\n id\n }\n viewer {\n user {\n id\n people(organizationId: $organizationId) {\n id\n }\n }\n id\n }\n}\n\nfragment VendorComplianceTabFragment on Vendor {\n complianceReports(first: 50) {\n edges {\n node {\n id\n ...VendorComplianceTabFragment_report\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment VendorComplianceTabFragment_report on VendorComplianceReport {\n id\n reportDate\n validUntil\n reportName\n file {\n fileName\n mimeType\n size\n id\n }\n}\n\nfragment VendorContactsTabFragment on Vendor {\n contacts(first: 50) {\n edges {\n node {\n id\n ...VendorContactsTabFragment_contact\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment VendorContactsTabFragment_contact on VendorContact {\n id\n fullName\n email\n phone\n role\n createdAt\n updatedAt\n}\n\nfragment VendorOverviewTabBusinessAssociateAgreementFragment on Vendor {\n businessAssociateAgreement {\n id\n fileName\n fileUrl\n validFrom\n validUntil\n createdAt\n }\n}\n\nfragment VendorOverviewTabDataPrivacyAgreementFragment on Vendor {\n dataPrivacyAgreement {\n id\n fileName\n fileUrl\n validFrom\n validUntil\n createdAt\n }\n}\n\nfragment VendorRiskAssessmentTabFragment on Vendor {\n id\n riskAssessments(first: 50) {\n edges {\n node {\n id\n ...VendorRiskAssessmentTabFragment_assessment\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment VendorRiskAssessmentTabFragment_assessment on VendorRiskAssessment {\n id\n createdAt\n expiresAt\n dataSensitivity\n businessImpact\n notes\n}\n\nfragment VendorServicesTabFragment on Vendor {\n services(first: 50) {\n edges {\n node {\n id\n ...VendorServicesTabFragment_service\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment VendorServicesTabFragment_service on VendorService {\n id\n name\n description\n createdAt\n updatedAt\n}\n\nfragment useVendorFormFragment on Vendor {\n id\n name\n description\n category\n statusPageUrl\n termsOfServiceUrl\n privacyPolicyUrl\n serviceLevelAgreementUrl\n dataProcessingAgreementUrl\n websiteUrl\n legalName\n headquarterAddress\n certifications\n countries\n securityPageUrl\n trustPageUrl\n businessOwner {\n id\n }\n securityOwner {\n id\n }\n}\n"
|
||||
"text": "query VendorGraphNodeQuery(\n $vendorId: ID!\n) {\n node(id: $vendorId) {\n __typename\n ... on Vendor {\n id\n snapshotId\n name\n websiteUrl\n ...useVendorFormFragment\n ...VendorComplianceTabFragment\n ...VendorContactsTabFragment\n ...VendorServicesTabFragment\n ...VendorRiskAssessmentTabFragment\n ...VendorOverviewTabBusinessAssociateAgreementFragment\n ...VendorOverviewTabDataPrivacyAgreementFragment\n }\n id\n }\n viewer {\n user {\n id\n }\n id\n }\n}\n\nfragment VendorComplianceTabFragment on Vendor {\n complianceReports(first: 50) {\n edges {\n node {\n id\n ...VendorComplianceTabFragment_report\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment VendorComplianceTabFragment_report on VendorComplianceReport {\n id\n reportDate\n validUntil\n reportName\n file {\n fileName\n mimeType\n size\n id\n }\n}\n\nfragment VendorContactsTabFragment on Vendor {\n contacts(first: 50) {\n edges {\n node {\n id\n ...VendorContactsTabFragment_contact\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment VendorContactsTabFragment_contact on VendorContact {\n id\n fullName\n email\n phone\n role\n createdAt\n updatedAt\n}\n\nfragment VendorOverviewTabBusinessAssociateAgreementFragment on Vendor {\n businessAssociateAgreement {\n id\n fileName\n fileUrl\n validFrom\n validUntil\n createdAt\n }\n}\n\nfragment VendorOverviewTabDataPrivacyAgreementFragment on Vendor {\n dataPrivacyAgreement {\n id\n fileName\n fileUrl\n validFrom\n validUntil\n createdAt\n }\n}\n\nfragment VendorRiskAssessmentTabFragment on Vendor {\n id\n riskAssessments(first: 50) {\n edges {\n node {\n id\n ...VendorRiskAssessmentTabFragment_assessment\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment VendorRiskAssessmentTabFragment_assessment on VendorRiskAssessment {\n id\n createdAt\n expiresAt\n dataSensitivity\n businessImpact\n notes\n}\n\nfragment VendorServicesTabFragment on Vendor {\n services(first: 50) {\n edges {\n node {\n id\n ...VendorServicesTabFragment_service\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n\nfragment VendorServicesTabFragment_service on VendorService {\n id\n name\n description\n createdAt\n updatedAt\n}\n\nfragment useVendorFormFragment on Vendor {\n id\n name\n description\n category\n statusPageUrl\n termsOfServiceUrl\n privacyPolicyUrl\n serviceLevelAgreementUrl\n dataProcessingAgreementUrl\n websiteUrl\n legalName\n headquarterAddress\n certifications\n countries\n securityPageUrl\n trustPageUrl\n businessOwner {\n id\n }\n securityOwner {\n id\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "5b907e5cc3446745ff2414ee06839aed";
|
||||
(node as any).hash = "0a13dd059626880c4a51f8a47b9c2423";
|
||||
|
||||
export default node;
|
||||
|
||||
@@ -32,6 +32,8 @@ import {
|
||||
IconPlusLarge,
|
||||
IconChevronDown,
|
||||
Avatar,
|
||||
IconPeopleAdd,
|
||||
Badge,
|
||||
} from "@probo/ui";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { graphql } from "relay-runtime";
|
||||
@@ -85,6 +87,9 @@ const OrganizationSelectorFragment = graphql`
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
invitations(first: 1, filter: {onlyPending: true}) {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -287,6 +292,7 @@ function OrganizationSelector({
|
||||
);
|
||||
|
||||
const organizations = data.organizations.edges.map((edge) => edge.node);
|
||||
const pendingInvitationsCount = data.invitations.totalCount;
|
||||
|
||||
const handleLoadMore = (e?: React.MouseEvent) => {
|
||||
e?.preventDefault();
|
||||
@@ -301,54 +307,79 @@ function OrganizationSelector({
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
toggle={
|
||||
<Button
|
||||
className="-ml-3"
|
||||
variant="tertiary"
|
||||
iconAfter={IconChevronGrabberVertical}
|
||||
>
|
||||
{currentOrganization?.name || ""}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div className="max-h-150 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent hover:scrollbar-thumb-gray-400">
|
||||
{organizations.map((organization) => (
|
||||
<DropdownItem
|
||||
asChild
|
||||
key={organization.id}
|
||||
<div className="flex items-center gap-1">
|
||||
<Dropdown
|
||||
toggle={
|
||||
<Button
|
||||
className="-ml-3"
|
||||
variant="tertiary"
|
||||
iconAfter={IconChevronGrabberVertical}
|
||||
>
|
||||
<Link to={`/organizations/${organization.id}`}>
|
||||
<Avatar src={organization.logoUrl} name={organization.name} />
|
||||
{organization.name}
|
||||
{currentOrganization?.name || ""}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div className="max-h-150 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent hover:scrollbar-thumb-gray-400">
|
||||
{organizations.map((organization) => (
|
||||
<DropdownItem
|
||||
asChild
|
||||
key={organization.id}
|
||||
>
|
||||
<Link to={`/organizations/${organization.id}`}>
|
||||
<Avatar src={organization.logoUrl} name={organization.name} />
|
||||
{organization.name}
|
||||
</Link>
|
||||
</DropdownItem>
|
||||
))}
|
||||
{hasNext && (
|
||||
<div className="px-3 py-1 flex justify-center">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
onClick={handleLoadMore}
|
||||
onMouseDown={(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="mx-auto"
|
||||
icon={IconChevronDown}
|
||||
disabled={isLoadingMore}
|
||||
>
|
||||
{isLoadingMore ? __("Loading...") : __("Show More")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DropdownSeparator />
|
||||
{pendingInvitationsCount > 0 && (
|
||||
<DropdownItem asChild>
|
||||
<Link to="/">
|
||||
<IconPeopleAdd size={16} />
|
||||
<span className="flex-1">{__("Invitations")}</span>
|
||||
<Badge variant="info" size="sm">
|
||||
{pendingInvitationsCount}
|
||||
</Badge>
|
||||
</Link>
|
||||
</DropdownItem>
|
||||
))}
|
||||
{hasNext && (
|
||||
<div className="px-3 py-1 flex justify-center">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
onClick={handleLoadMore}
|
||||
onMouseDown={(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="mx-auto"
|
||||
icon={IconChevronDown}
|
||||
disabled={isLoadingMore}
|
||||
>
|
||||
{isLoadingMore ? __("Loading...") : __("Show More")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DropdownSeparator />
|
||||
<DropdownItem asChild icon={IconPlusLarge}>
|
||||
<Link to="/organizations/new">
|
||||
<IconPlusLarge size={16} />
|
||||
{__("Add organization")}
|
||||
<DropdownItem asChild>
|
||||
<Link to="/organizations/new">
|
||||
<IconPlusLarge size={16} />
|
||||
{__("Add organization")}
|
||||
</Link>
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
{pendingInvitationsCount > 0 && (
|
||||
<Link to="/" className="relative" title={__("Invitations")}>
|
||||
<Button variant="tertiary" icon={IconPeopleAdd} />
|
||||
<Badge
|
||||
variant="info"
|
||||
size="sm"
|
||||
className="absolute -top-1 -right-1 min-w-[20px] h-5 flex items-center justify-center"
|
||||
>
|
||||
{pendingInvitationsCount}
|
||||
</Badge>
|
||||
</Link>
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<92c96c7b8a58554e9fb99b86489e13d4>>
|
||||
* @generated SignedSource<<7288534d3b5e852c463af820a9001298>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -208,6 +208,37 @@ return {
|
||||
"kind": "LinkedHandle",
|
||||
"name": "organizations"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": [
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "filter",
|
||||
"value": {
|
||||
"onlyPending": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "first",
|
||||
"value": 1
|
||||
}
|
||||
],
|
||||
"concreteType": "InvitationConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "invitations",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "totalCount",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": "invitations(filter:{\"onlyPending\":true},first:1)"
|
||||
},
|
||||
(v4/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
@@ -215,16 +246,16 @@ return {
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "57fa65c9d5f23210b606fbf0b2005fb8",
|
||||
"cacheID": "77f3fbbca18dda24151bff9901c54626",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "MainLayoutOrganizationSelectorPaginationQuery",
|
||||
"operationKind": "query",
|
||||
"text": "query MainLayoutOrganizationSelectorPaginationQuery(\n $after: CursorKey\n $first: Int = 25\n) {\n viewer {\n ...MainLayout_OrganizationSelector_viewer_2HEEH6\n id\n }\n}\n\nfragment MainLayout_OrganizationSelector_viewer_2HEEH6 on Viewer {\n organizations(first: $first, after: $after, orderBy: {field: NAME, direction: ASC}) {\n edges {\n node {\n id\n name\n logoUrl\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n}\n"
|
||||
"text": "query MainLayoutOrganizationSelectorPaginationQuery(\n $after: CursorKey\n $first: Int = 25\n) {\n viewer {\n ...MainLayout_OrganizationSelector_viewer_2HEEH6\n id\n }\n}\n\nfragment MainLayout_OrganizationSelector_viewer_2HEEH6 on Viewer {\n organizations(first: $first, after: $after, orderBy: {field: NAME, direction: ASC}) {\n edges {\n node {\n id\n name\n logoUrl\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n invitations(first: 1, filter: {onlyPending: true}) {\n totalCount\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "0dc2841aeeb6ba5291cb45d8644ec4af";
|
||||
(node as any).hash = "99a5cdcce2542bc7fba01d32bbd4648b";
|
||||
|
||||
export default node;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<0719379cc0124f36eb1810ad24b2ae5a>>
|
||||
* @generated SignedSource<<cb335e1fd93543371a82adf23002f600>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -275,6 +275,37 @@ return {
|
||||
"key": "MainLayout_OrganizationSelector_organizations",
|
||||
"kind": "LinkedHandle",
|
||||
"name": "organizations"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": [
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "filter",
|
||||
"value": {
|
||||
"status": "PENDING"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "first",
|
||||
"value": 1
|
||||
}
|
||||
],
|
||||
"concreteType": "InvitationConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "invitations",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "totalCount",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": "invitations(filter:{\"status\":\"PENDING\"},first:1)"
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
@@ -304,12 +335,12 @@ return {
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "6287440397ef427d84f8734b0953784c",
|
||||
"cacheID": "117a78fef8e7a8fe200baf1ebf2d2487",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "MainLayoutQuery",
|
||||
"operationKind": "query",
|
||||
"text": "query MainLayoutQuery(\n $organizationId: ID!\n) {\n viewer {\n id\n user {\n fullName\n email\n id\n }\n ...MainLayout_OrganizationSelector_viewer\n }\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n logoUrl\n }\n id\n }\n}\n\nfragment MainLayout_OrganizationSelector_viewer on Viewer {\n organizations(first: 25, orderBy: {field: NAME, direction: ASC}) {\n edges {\n node {\n id\n name\n logoUrl\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n}\n"
|
||||
"text": "query MainLayoutQuery(\n $organizationId: ID!\n) {\n viewer {\n id\n user {\n fullName\n email\n id\n }\n ...MainLayout_OrganizationSelector_viewer\n }\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n logoUrl\n }\n id\n }\n}\n\nfragment MainLayout_OrganizationSelector_viewer on Viewer {\n organizations(first: 25, orderBy: {field: NAME, direction: ASC}) {\n edges {\n node {\n id\n name\n logoUrl\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n invitations(first: 1, filter: {status: PENDING}) {\n totalCount\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<2eda1054dbabd2bb5e45d8db5c7a01fc>>
|
||||
* @generated SignedSource<<bd521d62fb0f29ace7af511b527ef750>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -11,6 +11,9 @@
|
||||
import { ReaderFragment } from 'relay-runtime';
|
||||
import { FragmentRefs } from "relay-runtime";
|
||||
export type MainLayout_OrganizationSelector_viewer$data = {
|
||||
readonly invitations: {
|
||||
readonly totalCount: number;
|
||||
};
|
||||
readonly organizations: {
|
||||
readonly edges: ReadonlyArray<{
|
||||
readonly node: {
|
||||
@@ -178,6 +181,37 @@ return {
|
||||
}
|
||||
],
|
||||
"storageKey": "__MainLayout_OrganizationSelector_organizations_connection(orderBy:{\"direction\":\"ASC\",\"field\":\"NAME\"})"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": [
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "filter",
|
||||
"value": {
|
||||
"status": "PENDING"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "first",
|
||||
"value": 1
|
||||
}
|
||||
],
|
||||
"concreteType": "InvitationConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "invitations",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "totalCount",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": "invitations(filter:{\"status\":\"PENDING\"},first:1)"
|
||||
}
|
||||
],
|
||||
"type": "Viewer",
|
||||
@@ -185,6 +219,6 @@ return {
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "0dc2841aeeb6ba5291cb45d8644ec4af";
|
||||
(node as any).hash = "d1491a8228d455e2491724c03c7c30c1";
|
||||
|
||||
export default node;
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
IconPlusLarge,
|
||||
} from "@probo/ui";
|
||||
import { usePageTitle } from "@probo/hooks";
|
||||
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
|
||||
import { formatDate } from "@probo/helpers";
|
||||
|
||||
const OrganizationsPageQuery = graphql`
|
||||
query OrganizationsPageQuery {
|
||||
@@ -25,6 +27,34 @@ const OrganizationsPageQuery = graphql`
|
||||
}
|
||||
}
|
||||
}
|
||||
invitations(first: 1000, orderBy: {field: CREATED_AT, direction: DESC}, filter: {onlyPending: true}) @connection(key: "OrganizationsPage_invitations") {
|
||||
__id
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
email
|
||||
fullName
|
||||
role
|
||||
expiresAt
|
||||
acceptedAt
|
||||
createdAt
|
||||
organization {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const acceptInvitationMutation = graphql`
|
||||
mutation OrganizationsPage_AcceptInvitationMutation($input: AcceptInvitationInput!) {
|
||||
acceptInvitation(input: $input) {
|
||||
invitation {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -41,15 +71,40 @@ export default function OrganizationsPage() {
|
||||
(edge) => edge.node
|
||||
);
|
||||
|
||||
const pendingInvitations = data.viewer.invitations.edges.map(
|
||||
(edge) => edge.node
|
||||
);
|
||||
|
||||
const [acceptInvitation, isAccepting] = useMutationWithToasts(
|
||||
acceptInvitationMutation,
|
||||
{
|
||||
successMessage: __("Invitation accepted successfully"),
|
||||
errorMessage: __("Failed to accept invitation"),
|
||||
}
|
||||
);
|
||||
|
||||
const handleAcceptInvitation = (invitationId: string, organizationId: string) => {
|
||||
acceptInvitation({
|
||||
variables: {
|
||||
input: {
|
||||
invitationId,
|
||||
},
|
||||
},
|
||||
onSuccess: () => {
|
||||
navigate(`/organizations/${organizationId}`);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
usePageTitle(__("Select an organization"));
|
||||
|
||||
useEffect(() => {
|
||||
if (organizations.length === 1) {
|
||||
if (organizations.length === 1 && pendingInvitations.length === 0) {
|
||||
navigate(`/organizations/${organizations[0].id}`);
|
||||
} else if (organizations.length === 0) {
|
||||
} else if (organizations.length === 0 && pendingInvitations.length === 0) {
|
||||
navigate("/organizations/new");
|
||||
}
|
||||
}, [organizations]);
|
||||
}, [organizations, pendingInvitations]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -58,12 +113,36 @@ export default function OrganizationsPage() {
|
||||
{__("Select an organization")}
|
||||
</h1>
|
||||
<div className="space-y-4 w-full">
|
||||
{organizations.map((organization) => (
|
||||
<OrganizationCard
|
||||
key={organization.id}
|
||||
organization={organization}
|
||||
/>
|
||||
))}
|
||||
{pendingInvitations.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h2 className="text-xl font-semibold">
|
||||
{__("Pending invitations")}
|
||||
</h2>
|
||||
{pendingInvitations.map((invitation) => (
|
||||
<InvitationCard
|
||||
key={invitation.id}
|
||||
invitation={invitation}
|
||||
onAccept={handleAcceptInvitation}
|
||||
isAccepting={isAccepting}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{organizations.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
{pendingInvitations.length > 0 && (
|
||||
<h2 className="text-xl font-semibold">
|
||||
{__("Your organizations")}
|
||||
</h2>
|
||||
)}
|
||||
{organizations.map((organization) => (
|
||||
<OrganizationCard
|
||||
key={organization.id}
|
||||
organization={organization}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<Card padded>
|
||||
<h2 className="text-xl font-semibold mb-1">
|
||||
{__("Create an organization")}
|
||||
@@ -86,6 +165,51 @@ export default function OrganizationsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
type InvitationCardProps = {
|
||||
invitation: {
|
||||
id: string;
|
||||
email: string;
|
||||
fullName: string;
|
||||
role: string;
|
||||
expiresAt: string;
|
||||
createdAt: string;
|
||||
organization: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
onAccept: (invitationId: string, organizationId: string) => void;
|
||||
isAccepting: boolean;
|
||||
};
|
||||
|
||||
function InvitationCard({ invitation, onAccept, isAccepting }: InvitationCardProps) {
|
||||
const { __ } = useTranslate();
|
||||
|
||||
return (
|
||||
<Card padded className="w-full">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1 space-y-1">
|
||||
<h3 className="text-lg font-semibold">
|
||||
{invitation.organization.name}
|
||||
</h3>
|
||||
<p className="text-sm text-txt-secondary">
|
||||
{__("Role")}: <span className="font-medium">{invitation.role}</span>
|
||||
</p>
|
||||
<p className="text-xs text-txt-tertiary">
|
||||
{__("Invited on")} {formatDate(invitation.createdAt)}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => onAccept(invitation.id, invitation.organization.id)}
|
||||
disabled={isAccepting}
|
||||
>
|
||||
{isAccepting ? __("Accepting...") : __("Accept invitation")}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
type OrganizationCardProps = {
|
||||
organization: {
|
||||
id: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<84d0e43eced78862d1c82603e9c332e8>>
|
||||
* @generated SignedSource<<c25588b7506704dd23d9c8be8672f9ef>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -12,6 +12,24 @@ import { ConcreteRequest } from 'relay-runtime';
|
||||
export type OrganizationsPageQuery$variables = Record<PropertyKey, never>;
|
||||
export type OrganizationsPageQuery$data = {
|
||||
readonly viewer: {
|
||||
readonly invitations: {
|
||||
readonly __id: string;
|
||||
readonly edges: ReadonlyArray<{
|
||||
readonly node: {
|
||||
readonly acceptedAt: any | null | undefined;
|
||||
readonly createdAt: any;
|
||||
readonly email: string;
|
||||
readonly expiresAt: any;
|
||||
readonly fullName: string;
|
||||
readonly id: string;
|
||||
readonly organization: {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
};
|
||||
readonly role: string;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
readonly organizations: {
|
||||
readonly __id: string;
|
||||
readonly edges: ReadonlyArray<{
|
||||
@@ -45,7 +63,65 @@ v1 = {
|
||||
"name": "id",
|
||||
"storageKey": null
|
||||
},
|
||||
v2 = [
|
||||
v2 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "name",
|
||||
"storageKey": null
|
||||
},
|
||||
v3 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "__typename",
|
||||
"storageKey": null
|
||||
},
|
||||
v4 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "cursor",
|
||||
"storageKey": null
|
||||
},
|
||||
v5 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "PageInfo",
|
||||
"kind": "LinkedField",
|
||||
"name": "pageInfo",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "endCursor",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "hasNextPage",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
v6 = {
|
||||
"kind": "ClientExtension",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "__id",
|
||||
"storageKey": null
|
||||
}
|
||||
]
|
||||
},
|
||||
v7 = [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
@@ -63,13 +139,7 @@ v2 = [
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v1/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "name",
|
||||
"storageKey": null
|
||||
},
|
||||
(v2/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
@@ -77,71 +147,129 @@ v2 = [
|
||||
"name": "logoUrl",
|
||||
"storageKey": null
|
||||
},
|
||||
(v3/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
(v4/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
(v5/*: any*/),
|
||||
(v6/*: any*/)
|
||||
],
|
||||
v8 = {
|
||||
"kind": "Literal",
|
||||
"name": "filter",
|
||||
"value": {
|
||||
"onlyPending": true
|
||||
}
|
||||
},
|
||||
v9 = {
|
||||
"kind": "Literal",
|
||||
"name": "orderBy",
|
||||
"value": {
|
||||
"direction": "DESC",
|
||||
"field": "CREATED_AT"
|
||||
}
|
||||
},
|
||||
v10 = [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "InvitationEdge",
|
||||
"kind": "LinkedField",
|
||||
"name": "edges",
|
||||
"plural": true,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Invitation",
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v1/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "__typename",
|
||||
"name": "email",
|
||||
"storageKey": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "fullName",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "role",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "expiresAt",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "acceptedAt",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "createdAt",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Organization",
|
||||
"kind": "LinkedField",
|
||||
"name": "organization",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v1/*: any*/),
|
||||
(v2/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
(v3/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "cursor",
|
||||
"storageKey": null
|
||||
}
|
||||
(v4/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "PageInfo",
|
||||
"kind": "LinkedField",
|
||||
"name": "pageInfo",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "endCursor",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "hasNextPage",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"kind": "ClientExtension",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "__id",
|
||||
"storageKey": null
|
||||
}
|
||||
]
|
||||
}
|
||||
(v5/*: any*/),
|
||||
(v6/*: any*/)
|
||||
],
|
||||
v3 = [
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "first",
|
||||
"value": 1000
|
||||
},
|
||||
v11 = {
|
||||
"kind": "Literal",
|
||||
"name": "first",
|
||||
"value": 1000
|
||||
},
|
||||
v12 = [
|
||||
(v11/*: any*/),
|
||||
(v0/*: any*/)
|
||||
],
|
||||
v13 = [
|
||||
(v8/*: any*/),
|
||||
(v11/*: any*/),
|
||||
(v9/*: any*/)
|
||||
];
|
||||
return {
|
||||
"fragment": {
|
||||
@@ -167,8 +295,21 @@ return {
|
||||
"kind": "LinkedField",
|
||||
"name": "__OrganizationsPage_organizations_connection",
|
||||
"plural": false,
|
||||
"selections": (v2/*: any*/),
|
||||
"selections": (v7/*: any*/),
|
||||
"storageKey": "__OrganizationsPage_organizations_connection(orderBy:{\"direction\":\"ASC\",\"field\":\"NAME\"})"
|
||||
},
|
||||
{
|
||||
"alias": "invitations",
|
||||
"args": [
|
||||
(v8/*: any*/),
|
||||
(v9/*: any*/)
|
||||
],
|
||||
"concreteType": "InvitationConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "__OrganizationsPage_invitations_connection",
|
||||
"plural": false,
|
||||
"selections": (v10/*: any*/),
|
||||
"storageKey": "__OrganizationsPage_invitations_connection(filter:{\"onlyPending\":true},orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})"
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
@@ -193,17 +334,17 @@ return {
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v3/*: any*/),
|
||||
"args": (v12/*: any*/),
|
||||
"concreteType": "OrganizationConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "organizations",
|
||||
"plural": false,
|
||||
"selections": (v2/*: any*/),
|
||||
"selections": (v7/*: any*/),
|
||||
"storageKey": "organizations(first:1000,orderBy:{\"direction\":\"ASC\",\"field\":\"NAME\"})"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v3/*: any*/),
|
||||
"args": (v12/*: any*/),
|
||||
"filters": [
|
||||
"orderBy"
|
||||
],
|
||||
@@ -212,6 +353,28 @@ return {
|
||||
"kind": "LinkedHandle",
|
||||
"name": "organizations"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v13/*: any*/),
|
||||
"concreteType": "InvitationConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "invitations",
|
||||
"plural": false,
|
||||
"selections": (v10/*: any*/),
|
||||
"storageKey": "invitations(filter:{\"onlyPending\":true},first:1000,orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v13/*: any*/),
|
||||
"filters": [
|
||||
"orderBy",
|
||||
"filter"
|
||||
],
|
||||
"handle": "connection",
|
||||
"key": "OrganizationsPage_invitations",
|
||||
"kind": "LinkedHandle",
|
||||
"name": "invitations"
|
||||
},
|
||||
(v1/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
@@ -219,7 +382,7 @@ return {
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "1735764e6816660969c5f96922320ac5",
|
||||
"cacheID": "5675b3eb7810ef04bf531d7bf988c86c",
|
||||
"id": null,
|
||||
"metadata": {
|
||||
"connection": [
|
||||
@@ -231,16 +394,25 @@ return {
|
||||
"viewer",
|
||||
"organizations"
|
||||
]
|
||||
},
|
||||
{
|
||||
"count": null,
|
||||
"cursor": null,
|
||||
"direction": "forward",
|
||||
"path": [
|
||||
"viewer",
|
||||
"invitations"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "OrganizationsPageQuery",
|
||||
"operationKind": "query",
|
||||
"text": "query OrganizationsPageQuery {\n viewer {\n organizations(first: 1000, orderBy: {field: NAME, direction: ASC}) {\n edges {\n node {\n id\n name\n logoUrl\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n id\n }\n}\n"
|
||||
"text": "query OrganizationsPageQuery {\n viewer {\n organizations(first: 1000, orderBy: {field: NAME, direction: ASC}) {\n edges {\n node {\n id\n name\n logoUrl\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n invitations(first: 1000, orderBy: {field: CREATED_AT, direction: DESC}, filter: {onlyPending: true}) {\n edges {\n node {\n id\n email\n fullName\n role\n expiresAt\n acceptedAt\n createdAt\n organization {\n id\n name\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n id\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "6fde39384e4678f88f17c34bbd30e684";
|
||||
(node as any).hash = "a548f9c3a434e12c079fb2ff651822d2";
|
||||
|
||||
export default node;
|
||||
|
||||
105
apps/console/src/pages/__generated__/OrganizationsPage_AcceptInvitationMutation.graphql.ts
generated
Normal file
105
apps/console/src/pages/__generated__/OrganizationsPage_AcceptInvitationMutation.graphql.ts
generated
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* @generated SignedSource<<b883cd0523e827b2c71b89e1d249d6dd>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
export type AcceptInvitationInput = {
|
||||
invitationId: string;
|
||||
};
|
||||
export type OrganizationsPage_AcceptInvitationMutation$variables = {
|
||||
input: AcceptInvitationInput;
|
||||
};
|
||||
export type OrganizationsPage_AcceptInvitationMutation$data = {
|
||||
readonly acceptInvitation: {
|
||||
readonly invitation: {
|
||||
readonly id: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
export type OrganizationsPage_AcceptInvitationMutation = {
|
||||
response: OrganizationsPage_AcceptInvitationMutation$data;
|
||||
variables: OrganizationsPage_AcceptInvitationMutation$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
var v0 = [
|
||||
{
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "input"
|
||||
}
|
||||
],
|
||||
v1 = [
|
||||
{
|
||||
"alias": null,
|
||||
"args": [
|
||||
{
|
||||
"kind": "Variable",
|
||||
"name": "input",
|
||||
"variableName": "input"
|
||||
}
|
||||
],
|
||||
"concreteType": "AcceptInvitationPayload",
|
||||
"kind": "LinkedField",
|
||||
"name": "acceptInvitation",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "Invitation",
|
||||
"kind": "LinkedField",
|
||||
"name": "invitation",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "id",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
];
|
||||
return {
|
||||
"fragment": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "OrganizationsPage_AcceptInvitationMutation",
|
||||
"selections": (v1/*: any*/),
|
||||
"type": "Mutation",
|
||||
"abstractKey": null
|
||||
},
|
||||
"kind": "Request",
|
||||
"operation": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Operation",
|
||||
"name": "OrganizationsPage_AcceptInvitationMutation",
|
||||
"selections": (v1/*: any*/)
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "cc4a442037edaa624948b5be0c009823",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "OrganizationsPage_AcceptInvitationMutation",
|
||||
"operationKind": "mutation",
|
||||
"text": "mutation OrganizationsPage_AcceptInvitationMutation(\n $input: AcceptInvitationInput!\n) {\n acceptInvitation(input: $input) {\n invitation {\n id\n }\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "190213ab6fdc068343a270b4fa94e160";
|
||||
|
||||
export default node;
|
||||
@@ -1,35 +1,49 @@
|
||||
import { Link, useNavigate } from "react-router";
|
||||
import { Link, useNavigate, useSearchParams } from "react-router";
|
||||
import { Button, Field, useToast } from "@probo/ui";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { z } from "zod";
|
||||
import { useFormWithSchema } from "/hooks/useFormWithSchema";
|
||||
import { usePageTitle } from "@probo/hooks";
|
||||
import { buildEndpoint } from "/providers/RelayProviders";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const schema = z.object({
|
||||
fullName: z.string().min(2),
|
||||
password: z.string().min(8),
|
||||
});
|
||||
|
||||
export default function ConfirmInvitationPage() {
|
||||
export default function SignupFromInvitationPage() {
|
||||
const { __ } = useTranslate();
|
||||
const navigate = useNavigate();
|
||||
const { toast } = useToast();
|
||||
const { register, handleSubmit, formState } = useFormWithSchema(
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const { register, handleSubmit, formState, reset } = useFormWithSchema(
|
||||
schema,
|
||||
{
|
||||
defaultValues: {
|
||||
fullName: "",
|
||||
password: "",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const fullNameFromParams = searchParams.get("fullName") || "";
|
||||
if (fullNameFromParams) {
|
||||
reset({
|
||||
fullName: fullNameFromParams,
|
||||
password: "",
|
||||
});
|
||||
}
|
||||
}, [searchParams, reset]);
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
const token = searchParams.get("token");
|
||||
|
||||
if (!token) {
|
||||
toast({
|
||||
title: __("Confirmation failed"),
|
||||
title: __("Signup failed"),
|
||||
description: __("Invalid or missing invitation token"),
|
||||
variant: "error",
|
||||
});
|
||||
@@ -37,7 +51,7 @@ export default function ConfirmInvitationPage() {
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
buildEndpoint("/api/console/v1/auth/invitation"),
|
||||
buildEndpoint("/api/console/v1/auth/signup-from-invitation"),
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -47,16 +61,16 @@ export default function ConfirmInvitationPage() {
|
||||
body: JSON.stringify({
|
||||
token: token,
|
||||
password: data.password,
|
||||
fullName: data.fullName,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
// Registration failed
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
toast({
|
||||
title: __("Confirmation failed"),
|
||||
description: errorData.message || __("Confirmation failed"),
|
||||
title: __("Signup failed"),
|
||||
description: errorData.message || __("Signup failed"),
|
||||
variant: "error",
|
||||
});
|
||||
return;
|
||||
@@ -64,24 +78,33 @@ export default function ConfirmInvitationPage() {
|
||||
|
||||
toast({
|
||||
title: __("Success"),
|
||||
description: __("Invitation confirmed successfully"),
|
||||
description: __("Account created successfully. Please accept your invitation to join the organization."),
|
||||
variant: "success",
|
||||
});
|
||||
navigate("/", { replace: true });
|
||||
});
|
||||
|
||||
usePageTitle(__("Confirm invitation"));
|
||||
usePageTitle(__("Create your account"));
|
||||
|
||||
return (
|
||||
<div className="space-y-6 w-full max-w-md mx-auto">
|
||||
<div className="space-y-2 text-center">
|
||||
<h1 className="text-3xl font-bold">{__("Confirm invitation")}</h1>
|
||||
<h1 className="text-3xl font-bold">{__("Create your account")}</h1>
|
||||
<p className="text-txt-tertiary">
|
||||
{__("Enter your information to confirm your invitation")}
|
||||
{__("Set your password to join the organization")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={onSubmit} className="space-y-4">
|
||||
<Field
|
||||
label={__("Full Name")}
|
||||
type="text"
|
||||
placeholder={__("John Doe")}
|
||||
{...register("fullName")}
|
||||
required
|
||||
error={formState.errors.fullName?.message}
|
||||
/>
|
||||
|
||||
<Field
|
||||
label={__("Password")}
|
||||
type="password"
|
||||
@@ -93,8 +116,8 @@ export default function ConfirmInvitationPage() {
|
||||
|
||||
<Button type="submit" className="w-full" disabled={formState.isLoading}>
|
||||
{formState.isLoading
|
||||
? __("Confirming invitation...")
|
||||
: __("Confirm invitation")}
|
||||
? __("Creating account...")
|
||||
: __("Create account")}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
@@ -50,13 +50,13 @@ import { sprintf } from "@probo/helpers";
|
||||
import { useFormWithSchema } from "/hooks/useFormWithSchema";
|
||||
import { z } from "zod";
|
||||
import type { NodeOf } from "/types";
|
||||
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
|
||||
import { useOrganizationId } from "/hooks/useOrganizationId";
|
||||
import { InviteUserDialog } from "/components/organizations/InviteUserDialog";
|
||||
import { useDeleteOrganizationMutation } from "/hooks/graph/OrganizationGraph";
|
||||
import { useNavigate } from "react-router";
|
||||
import { DeleteOrganizationDialog } from "/components/organizations/DeleteOrganizationDialog";
|
||||
import { CustomDomainManager } from "/components/customDomains/CustomDomainManager";
|
||||
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
|
||||
|
||||
const organizationSchema = z.object({
|
||||
name: z.string().min(1, "Organization name is required"),
|
||||
@@ -220,6 +220,7 @@ const deleteHorizontalLogoMutation = graphql`
|
||||
export default function SettingsPage({ queryRef }: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const navigate = useNavigate();
|
||||
const organizationId = useOrganizationId();
|
||||
const organizationKey = usePreloadedQuery(
|
||||
organizationViewQuery,
|
||||
queryRef
|
||||
@@ -240,6 +241,14 @@ export default function SettingsPage({ queryRef }: Props) {
|
||||
organizationKey as SettingsPageInvitationsFragment$key
|
||||
);
|
||||
|
||||
const refetchMemberships = () => {
|
||||
membershipsPagination.refetch({}, { fetchPolicy: 'network-only' });
|
||||
};
|
||||
|
||||
const refetchInvitations = () => {
|
||||
invitationsPagination.refetch({}, { fetchPolicy: 'network-only' });
|
||||
};
|
||||
|
||||
const [updateOrganization] = useMutation(updateOrganizationMutation);
|
||||
const [deleteHorizontalLogo, isDeletingHorizontalLogo] = useMutationWithToasts(
|
||||
deleteHorizontalLogoMutation,
|
||||
@@ -253,24 +262,6 @@ export default function SettingsPage({ queryRef }: Props) {
|
||||
const invitations = invitationsPagination.data.invitations?.edges.map((edge) => edge.node) || [];
|
||||
const [activeTab, setActiveTab] = useState<"memberships" | "invitations">("memberships");
|
||||
|
||||
const refetchMemberships = ({ order }: { order: { direction: string; field: string } }) => {
|
||||
membershipsPagination.refetch({
|
||||
order: {
|
||||
direction: order.direction as "ASC" | "DESC",
|
||||
field: order.field as "CREATED_AT" | "FULL_NAME" | "EMAIL_ADDRESS" | "ROLE"
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const refetchInvitations = ({ order }: { order: { direction: string; field: string } }) => {
|
||||
invitationsPagination.refetch({
|
||||
order: {
|
||||
direction: order.direction as "ASC" | "DESC",
|
||||
field: order.field as "CREATED_AT" | "EXPIRES_AT" | "FULL_NAME" | "EMAIL" | "ROLE" | "STATUS" | "ACCEPTED_AT"
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const { formState, handleSubmit, register, reset } = useFormWithSchema(
|
||||
organizationSchema,
|
||||
{
|
||||
@@ -572,7 +563,10 @@ export default function SettingsPage({ queryRef }: Props) {
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-base font-medium">{__("Workspace members")}</h2>
|
||||
<InviteUserDialog connectionId={invitationsPagination.data.invitations?.__id}>
|
||||
<InviteUserDialog
|
||||
connectionId={invitationsPagination.data.invitations?.__id}
|
||||
onRefetch={refetchInvitations}
|
||||
>
|
||||
<Button variant="secondary">{__("Invite member")}</Button>
|
||||
</InviteUserDialog>
|
||||
</div>
|
||||
@@ -603,7 +597,14 @@ export default function SettingsPage({ queryRef }: Props) {
|
||||
{activeTab === "memberships" && (
|
||||
<SortableTable
|
||||
{...membershipsPagination}
|
||||
refetch={refetchMemberships}
|
||||
refetch={({ order }: { order: { direction: string; field: string } }) => {
|
||||
membershipsPagination.refetch({
|
||||
order: {
|
||||
direction: order.direction as "ASC" | "DESC",
|
||||
field: order.field as "CREATED_AT" | "FULL_NAME" | "EMAIL_ADDRESS" | "ROLE"
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Thead>
|
||||
<Tr>
|
||||
@@ -623,7 +624,13 @@ export default function SettingsPage({ queryRef }: Props) {
|
||||
</Tr>
|
||||
) : (
|
||||
memberships.map((membership) => (
|
||||
<MembershipRow key={membership.id} membership={membership} />
|
||||
<MembershipRow
|
||||
key={membership.id}
|
||||
membership={membership}
|
||||
connectionId={membershipsPagination.data.memberships?.__id}
|
||||
organizationId={organizationId}
|
||||
onRefetch={refetchMemberships}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</Tbody>
|
||||
@@ -633,7 +640,14 @@ export default function SettingsPage({ queryRef }: Props) {
|
||||
{activeTab === "invitations" && (
|
||||
<SortableTable
|
||||
{...invitationsPagination}
|
||||
refetch={refetchInvitations}
|
||||
refetch={({ order }: { order: { direction: string; field: string } }) => {
|
||||
invitationsPagination.refetch({
|
||||
order: {
|
||||
direction: order.direction as "ASC" | "DESC",
|
||||
field: order.field as "CREATED_AT" | "EXPIRES_AT" | "FULL_NAME" | "EMAIL" | "ROLE" | "STATUS" | "ACCEPTED_AT"
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Thead>
|
||||
<Tr>
|
||||
@@ -659,6 +673,8 @@ export default function SettingsPage({ queryRef }: Props) {
|
||||
key={invitation.id}
|
||||
invitation={invitation}
|
||||
connectionId={invitationsPagination.data.invitations?.__id}
|
||||
organizationId={organizationId}
|
||||
onRefetch={refetchInvitations}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
@@ -794,9 +810,12 @@ function Connectors(props: {
|
||||
}
|
||||
|
||||
const removeMemberMutation = graphql`
|
||||
mutation SettingsPage_RemoveMemberMutation($input: RemoveMemberInput!) {
|
||||
mutation SettingsPage_RemoveMemberMutation(
|
||||
$input: RemoveMemberInput!
|
||||
$connections: [ID!]!
|
||||
) {
|
||||
removeMember(input: $input) {
|
||||
success
|
||||
deletedMemberId @deleteEdge(connections: $connections)
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -804,16 +823,16 @@ const removeMemberMutation = graphql`
|
||||
function InvitationRow(props: {
|
||||
invitation: NodeOf<SettingsPageInvitationsFragment$data["invitations"]>;
|
||||
connectionId?: string;
|
||||
organizationId: string;
|
||||
onRefetch: () => void;
|
||||
}) {
|
||||
const { __ } = useTranslate();
|
||||
const confirm = useConfirm();
|
||||
const [deleteInvitation, isDeleting] = useMutationWithToasts(
|
||||
deleteInvitationMutation,
|
||||
{
|
||||
successMessage: sprintf(
|
||||
__("Invitation for %s deleted successfully"),
|
||||
props.invitation.fullName
|
||||
),
|
||||
successMessage: __("Invitation deleted successfully"),
|
||||
errorMessage: __("Failed to delete invitation"),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -830,6 +849,9 @@ function InvitationRow(props: {
|
||||
},
|
||||
connections: props.connectionId ? [props.connectionId] : [],
|
||||
},
|
||||
onCompleted: () => {
|
||||
props.onRefetch();
|
||||
},
|
||||
});
|
||||
},
|
||||
{
|
||||
@@ -885,15 +907,16 @@ function InvitationRow(props: {
|
||||
);
|
||||
}
|
||||
|
||||
function MembershipRow(props: { membership: NodeOf<SettingsPageMembershipsFragment$data["memberships"]> }) {
|
||||
function MembershipRow(props: {
|
||||
membership: NodeOf<SettingsPageMembershipsFragment$data["memberships"]>;
|
||||
connectionId?: string;
|
||||
organizationId: string;
|
||||
onRefetch: () => void;
|
||||
}) {
|
||||
const { __ } = useTranslate();
|
||||
const organizationId = useOrganizationId();
|
||||
const [removeMember, isRemoving] = useMutationWithToasts(removeMemberMutation, {
|
||||
successMessage: sprintf(
|
||||
__("Member %s removed successfully"),
|
||||
props.membership.fullName
|
||||
),
|
||||
errorMessage: sprintf(__("Failed to remove member %s"), props.membership.fullName),
|
||||
successMessage: __("Member removed successfully"),
|
||||
errorMessage: __("Failed to remove member"),
|
||||
});
|
||||
const confirm = useConfirm();
|
||||
const [isRemoved, setIsRemoved] = useState(false);
|
||||
@@ -909,11 +932,13 @@ function MembershipRow(props: { membership: NodeOf<SettingsPageMembershipsFragme
|
||||
variables: {
|
||||
input: {
|
||||
memberId: props.membership.id,
|
||||
organizationId: organizationId,
|
||||
organizationId: props.organizationId,
|
||||
},
|
||||
connections: props.connectionId ? [props.connectionId] : [],
|
||||
},
|
||||
onSuccess: () => {
|
||||
onCompleted: () => {
|
||||
setIsRemoved(true);
|
||||
props.onRefetch();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<4f099d1ee6b4635ca8129eca77d3f7e8>>
|
||||
* @generated SignedSource<<09561c2c29459fc840773b414af6083a>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -9,6 +9,7 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import { ReaderFragment } from 'relay-runtime';
|
||||
export type InvitationStatus = "ACCEPTED" | "EXPIRED" | "PENDING";
|
||||
import { FragmentRefs } from "relay-runtime";
|
||||
export type SettingsPageInvitationsFragment$data = {
|
||||
readonly id: string;
|
||||
@@ -23,6 +24,7 @@ export type SettingsPageInvitationsFragment$data = {
|
||||
readonly fullName: string;
|
||||
readonly id: string;
|
||||
readonly role: string;
|
||||
readonly status: InvitationStatus;
|
||||
};
|
||||
}>;
|
||||
readonly totalCount: number;
|
||||
@@ -171,6 +173,13 @@ return {
|
||||
"name": "role",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "status",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
@@ -273,6 +282,6 @@ return {
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "d971d93653991efde284ed2b87068698";
|
||||
(node as any).hash = "f9a1ec38579cea21312ba0a20bb7394a";
|
||||
|
||||
export default node;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<994eb713978ecef3329fd9873e4c744c>>
|
||||
* @generated SignedSource<<d210161405099a17aa25d06ca4563634>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -14,11 +14,12 @@ export type RemoveMemberInput = {
|
||||
organizationId: string;
|
||||
};
|
||||
export type SettingsPage_RemoveMemberMutation$variables = {
|
||||
connections: ReadonlyArray<string>;
|
||||
input: RemoveMemberInput;
|
||||
};
|
||||
export type SettingsPage_RemoveMemberMutation$data = {
|
||||
readonly removeMember: {
|
||||
readonly success: boolean;
|
||||
readonly deletedMemberId: string;
|
||||
};
|
||||
};
|
||||
export type SettingsPage_RemoveMemberMutation = {
|
||||
@@ -27,67 +28,106 @@ export type SettingsPage_RemoveMemberMutation = {
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
var v0 = [
|
||||
var v0 = {
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "connections"
|
||||
},
|
||||
v1 = {
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "input"
|
||||
},
|
||||
v2 = [
|
||||
{
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "input"
|
||||
"kind": "Variable",
|
||||
"name": "input",
|
||||
"variableName": "input"
|
||||
}
|
||||
],
|
||||
v1 = [
|
||||
{
|
||||
"alias": null,
|
||||
"args": [
|
||||
{
|
||||
"kind": "Variable",
|
||||
"name": "input",
|
||||
"variableName": "input"
|
||||
}
|
||||
],
|
||||
"concreteType": "RemoveMemberPayload",
|
||||
"kind": "LinkedField",
|
||||
"name": "removeMember",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "success",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
];
|
||||
v3 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "deletedMemberId",
|
||||
"storageKey": null
|
||||
};
|
||||
return {
|
||||
"fragment": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"argumentDefinitions": [
|
||||
(v0/*: any*/),
|
||||
(v1/*: any*/)
|
||||
],
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "SettingsPage_RemoveMemberMutation",
|
||||
"selections": (v1/*: any*/),
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v2/*: any*/),
|
||||
"concreteType": "RemoveMemberPayload",
|
||||
"kind": "LinkedField",
|
||||
"name": "removeMember",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v3/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"type": "Mutation",
|
||||
"abstractKey": null
|
||||
},
|
||||
"kind": "Request",
|
||||
"operation": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"argumentDefinitions": [
|
||||
(v1/*: any*/),
|
||||
(v0/*: any*/)
|
||||
],
|
||||
"kind": "Operation",
|
||||
"name": "SettingsPage_RemoveMemberMutation",
|
||||
"selections": (v1/*: any*/)
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v2/*: any*/),
|
||||
"concreteType": "RemoveMemberPayload",
|
||||
"kind": "LinkedField",
|
||||
"name": "removeMember",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v3/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"filters": null,
|
||||
"handle": "deleteEdge",
|
||||
"key": "",
|
||||
"kind": "ScalarHandle",
|
||||
"name": "deletedMemberId",
|
||||
"handleArgs": [
|
||||
{
|
||||
"kind": "Variable",
|
||||
"name": "connections",
|
||||
"variableName": "connections"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "97e29046871ce8aab01abf98a62236fc",
|
||||
"cacheID": "e2dd0f4d7327ce3bc97754c85d3f700d",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "SettingsPage_RemoveMemberMutation",
|
||||
"operationKind": "mutation",
|
||||
"text": "mutation SettingsPage_RemoveMemberMutation(\n $input: RemoveMemberInput!\n) {\n removeMember(input: $input) {\n success\n }\n}\n"
|
||||
"text": "mutation SettingsPage_RemoveMemberMutation(\n $input: RemoveMemberInput!\n) {\n removeMember(input: $input) {\n deletedMemberId\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "f61071a0fb6f6554e56e79b6b04bc135";
|
||||
(node as any).hash = "9909a8b95f8d8621ffdf02da34ec8da2";
|
||||
|
||||
export default node;
|
||||
|
||||
@@ -74,8 +74,8 @@ const routes = [
|
||||
Component: lazy(() => import("./pages/auth/ConfirmEmailPage")),
|
||||
},
|
||||
{
|
||||
path: "confirm-invitation",
|
||||
Component: lazy(() => import("./pages/auth/ConfirmInvitationPage")),
|
||||
path: "signup-from-invitation",
|
||||
Component: lazy(() => import("./pages/auth/SignupFromInvitationPage")),
|
||||
},
|
||||
{
|
||||
path: "forgot-password",
|
||||
|
||||
@@ -30,10 +30,9 @@ export const vendorRoutes = [
|
||||
{
|
||||
path: "vendors/:vendorId",
|
||||
fallback: PageSkeleton,
|
||||
queryLoader: ({ vendorId, organizationId }) =>
|
||||
queryLoader: ({ vendorId }) =>
|
||||
loadQuery(relayEnvironment, vendorNodeQuery, {
|
||||
vendorId,
|
||||
organizationId,
|
||||
}),
|
||||
Component: lazy(
|
||||
() => import("../pages/organizations/vendors/VendorDetailPage")
|
||||
@@ -95,10 +94,9 @@ export const vendorRoutes = [
|
||||
{
|
||||
path: "snapshots/:snapshotId/vendors/:vendorId",
|
||||
fallback: PageSkeleton,
|
||||
queryLoader: ({ vendorId, organizationId }) =>
|
||||
queryLoader: ({ vendorId }) =>
|
||||
loadQuery(relayEnvironment, vendorNodeQuery, {
|
||||
vendorId,
|
||||
organizationId,
|
||||
}),
|
||||
Component: lazy(
|
||||
() => import("../pages/organizations/vendors/VendorDetailPage")
|
||||
|
||||
Reference in New Issue
Block a user