Replug UserDropdown -> SessionDropdown

Signed-off-by: Émile Ré <nemile.re@gmail.com>
This commit is contained in:
Émile Ré
2025-12-19 22:36:02 +01:00
committed by Bryan Frimin
parent 4218200409
commit 2aeae21c41
8 changed files with 355 additions and 42 deletions

View File

@@ -24,7 +24,7 @@ export default function SignInPage() {
const [signIn, isSigningIn] = useMutation<SignInPageMutation>(signInMutation);
const handlePasswordLogin: FormEventHandler<HTMLFormElement> = async (e) => {
const handlePasswordLogin: FormEventHandler<HTMLFormElement> = (e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const emailValue = formData.get("email")?.toString();
@@ -32,24 +32,24 @@ export default function SignInPage() {
if (!emailValue || !passwordValue) return;
try {
signIn({
variables: {
input: {
email: emailValue,
password: passwordValue,
},
signIn({
variables: {
input: {
email: emailValue,
password: passwordValue,
},
});
window.location.href = "/";
} catch (e: unknown) {
toast({
title: __("Error"),
description: e instanceof Error ? e.message : __("Failed to login"),
variant: "error",
});
}
},
onCompleted: () => {
window.location.href = "/";
},
onError: (e) => {
toast({
title: __("Error"),
description: e instanceof Error ? e.message : __("Failed to login"),
variant: "error",
});
},
});
};
return (

View File

@@ -5,12 +5,14 @@ import {
} from "react-relay";
import { organizationLayoutQuery } from "./OrganizationLayoutQuery";
import { Outlet } from "react-router";
import { Layout } from "@probo/ui";
import { Layout, Skeleton } from "@probo/ui";
import { Sidebar } from "./_components/Sidebar";
import { OrganizationDropdown } from "./_components/OrganizationDropdown";
import { consoleEnvironment } from "/environments";
import type { OrganizationLayoutQuery } from "./__generated__/OrganizationLayoutQuery.graphql";
import { PermissionsProvider } from "/providers/PermissionsProvider";
import { SessionDropdown } from "./_components/SessionDropdown";
import { Suspense } from "react";
interface OrganizationLayoutProps {
queryRef: PreloadedQuery<OrganizationLayoutQuery>;
@@ -32,9 +34,9 @@ export default function OrganizationLayout(props: OrganizationLayoutProps) {
<div className="mr-auto">
<OrganizationDropdown fKey={data.organization} />
</div>
{/* <Suspense fallback={<Skeleton className="w-32 h-8" />}>
<UserDropdown organizationId={organizationId} />
</Suspense> */}
<Suspense fallback={<Skeleton className="w-32 h-8" />}>
<SessionDropdown fKey={data.viewer} />
</Suspense>
</>
}
sidebar={<Sidebar />}

View File

@@ -15,10 +15,11 @@ export const organizationLayoutQuery = graphql`
...OrganizationDropdownFragment
}
}
viewer {
viewer @required(action: THROW) {
pendingInvitations {
totalCount
}
...SessionDropdownFragment @arguments(organizationId: $organizationId)
}
}
`;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<e99ea3ab9f033b6a5384d389dd964f06>>
* @generated SignedSource<<1d82d885a77675a330799121edb5cb69>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -21,7 +21,8 @@ export type OrganizationLayoutQuery$data = {
readonly pendingInvitations: {
readonly totalCount: number | null | undefined;
} | null | undefined;
} | null | undefined;
readonly " $fragmentSpreads": FragmentRefs<"SessionDropdownFragment">;
};
};
export type OrganizationLayoutQuery = {
response: OrganizationLayoutQuery$data;
@@ -61,7 +62,14 @@ v2 = {
],
"storageKey": null
},
v3 = {
v3 = [
{
"kind": "Variable",
"name": "organizationId",
"variableName": "organizationId"
}
],
v4 = {
"alias": null,
"args": null,
"kind": "ScalarField",
@@ -103,16 +111,25 @@ return {
"action": "THROW"
},
{
"alias": null,
"args": null,
"concreteType": "Identity",
"kind": "LinkedField",
"name": "viewer",
"plural": false,
"selections": [
(v2/*: any*/)
],
"storageKey": null
"kind": "RequiredField",
"field": {
"alias": null,
"args": null,
"concreteType": "Identity",
"kind": "LinkedField",
"name": "viewer",
"plural": false,
"selections": [
(v2/*: any*/),
{
"args": (v3/*: any*/),
"kind": "FragmentSpread",
"name": "SessionDropdownFragment"
}
],
"storageKey": null
},
"action": "THROW"
}
],
"type": "Query",
@@ -153,7 +170,7 @@ return {
"type": "Organization",
"abstractKey": null
},
(v3/*: any*/)
(v4/*: any*/)
],
"storageKey": null
},
@@ -166,23 +183,56 @@ return {
"plural": false,
"selections": [
(v2/*: any*/),
(v3/*: any*/)
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "email",
"storageKey": null
},
{
"alias": null,
"args": (v3/*: any*/),
"concreteType": "IdentityProfile",
"kind": "LinkedField",
"name": "profileFor",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "firstName",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "lastName",
"storageKey": null
},
(v4/*: any*/)
],
"storageKey": null
},
(v4/*: any*/)
],
"storageKey": null
}
]
},
"params": {
"cacheID": "b991c1a406cc9c0df5c67099285c31a8",
"cacheID": "0ebb386f89d19888f18a05c8dc573895",
"id": null,
"metadata": {},
"name": "OrganizationLayoutQuery",
"operationKind": "query",
"text": "query OrganizationLayoutQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n ...OrganizationDropdownFragment\n }\n id\n }\n viewer {\n pendingInvitations {\n totalCount\n }\n id\n }\n}\n\nfragment OrganizationDropdownFragment on Organization {\n name\n}\n"
"text": "query OrganizationLayoutQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n ...OrganizationDropdownFragment\n }\n id\n }\n viewer {\n pendingInvitations {\n totalCount\n }\n ...SessionDropdownFragment_4xMPKw\n id\n }\n}\n\nfragment OrganizationDropdownFragment on Organization {\n name\n}\n\nfragment SessionDropdownFragment_4xMPKw on Identity {\n email\n profileFor(organizationId: $organizationId) {\n firstName\n lastName\n id\n }\n}\n"
}
};
})();
(node as any).hash = "2e8a1c50dc999d218830ab3df10cdeb5";
(node as any).hash = "d3d91888eabb36cc450ca585757b21c7";
export default node;

View File

@@ -0,0 +1,104 @@
import {
DropdownSeparator,
IconArrowBoxLeft,
IconCircleQuestionmark,
IconKey,
IconPageTextLine,
UserDropdown,
UserDropdownItem,
useToast,
} from "@probo/ui";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
import { graphql } from "relay-runtime";
import { useOrganizationId } from "/hooks/useOrganizationId";
import { useFragment, useMutation } from "react-relay";
import { useTranslate } from "@probo/i18n";
import type { SessionDropdownFragment$key } from "./__generated__/SessionDropdownFragment.graphql";
export const fragment = graphql`
fragment SessionDropdownFragment on Identity
@argumentDefinitions(organizationId: { type: "ID!" }) {
email
profileFor(organizationId: $organizationId) {
firstName
lastName
}
}
`;
const signOutMutation = graphql`
mutation SessionDropdownSignOutMutation {
signOut {
success
}
}
`;
export function SessionDropdown(props: { fKey: SessionDropdownFragment$key }) {
const { fKey } = props;
const { __ } = useTranslate();
const organizationId = useOrganizationId();
const { isAuthorized } = use(PermissionsContext);
const { toast } = useToast();
const { email, profileFor: profile } =
useFragment<SessionDropdownFragment$key>(fragment, fKey);
const [signOut] = useMutation(signOutMutation);
const handleLogout: React.MouseEventHandler<HTMLAnchorElement> = (e) => {
e.preventDefault();
signOut({
variables: {},
onCompleted: () => {
window.location.reload();
},
onError: (e) => {
toast({
title: __("Error"),
description: e.message as string,
variant: "error",
});
},
});
};
// FIXME: user should always have a default profile
const fullName = profile?.firstName
? `${profile.firstName} ${profile.lastName}`
: email;
return (
<UserDropdown fullName={fullName} email={email}>
{isAuthorized("Organization", "deleteOrganization") && (
<UserDropdownItem
to="/api-keys"
icon={IconKey}
label={__("API Keys")}
/>
)}
{isAuthorized("Organization", "listSignableDocuments") && (
<UserDropdownItem
to={`/organizations/${organizationId}/employee`}
icon={IconPageTextLine}
label={__("My Signatures")}
/>
)}
<UserDropdownItem
to="mailto:support@getprobo.com"
icon={IconCircleQuestionmark}
label={__("Help")}
/>
<DropdownSeparator />
<UserDropdownItem
variant="danger"
to="/logout"
icon={IconArrowBoxLeft}
label="Logout"
onClick={handleLogout}
/>
</UserDropdown>
);
}

View File

@@ -32,7 +32,6 @@ export function Sidebar() {
const prefix = `/organizations/${organizationId}`;
console.log(isAuthorized("Organization", "listMeetings"));
return (
<ul className="space-y-[2px]">
{isAuthorized("Organization", "listMeetings") && (

View File

@@ -0,0 +1,83 @@
/**
* @generated SignedSource<<ccb8e318151fff4495fb2eaacc671612>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type SessionDropdownFragment$data = {
readonly email: any;
readonly profileFor: {
readonly firstName: string | null | undefined;
readonly lastName: string | null | undefined;
} | null | undefined;
readonly " $fragmentType": "SessionDropdownFragment";
};
export type SessionDropdownFragment$key = {
readonly " $data"?: SessionDropdownFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"SessionDropdownFragment">;
};
const node: ReaderFragment = {
"argumentDefinitions": [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "organizationId"
}
],
"kind": "Fragment",
"metadata": null,
"name": "SessionDropdownFragment",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "email",
"storageKey": null
},
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "organizationId",
"variableName": "organizationId"
}
],
"concreteType": "IdentityProfile",
"kind": "LinkedField",
"name": "profileFor",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "firstName",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "lastName",
"storageKey": null
}
],
"storageKey": null
}
],
"type": "Identity",
"abstractKey": null
};
(node as any).hash = "de1d0f069e5648898cab6a56a394d86f";
export default node;

View File

@@ -0,0 +1,74 @@
/**
* @generated SignedSource<<8cb399eb2f38eaf3059531ea4bc3fa30>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type SessionDropdownSignOutMutation$variables = Record<PropertyKey, never>;
export type SessionDropdownSignOutMutation$data = {
readonly signOut: {
readonly success: boolean;
} | null | undefined;
};
export type SessionDropdownSignOutMutation = {
response: SessionDropdownSignOutMutation$data;
variables: SessionDropdownSignOutMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"alias": null,
"args": null,
"concreteType": "SignOutPayload",
"kind": "LinkedField",
"name": "signOut",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "success",
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "SessionDropdownSignOutMutation",
"selections": (v0/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [],
"kind": "Operation",
"name": "SessionDropdownSignOutMutation",
"selections": (v0/*: any*/)
},
"params": {
"cacheID": "a677190a127d7f75d5c629fab236a7a3",
"id": null,
"metadata": {},
"name": "SessionDropdownSignOutMutation",
"operationKind": "mutation",
"text": "mutation SessionDropdownSignOutMutation {\n signOut {\n success\n }\n}\n"
}
};
})();
(node as any).hash = "10c9bdf35ab3961c7488973e052cf978";
export default node;