Plug back general settings page

Signed-off-by: Émile Ré <nemile.re@gmail.com>
This commit is contained in:
Émile Ré
2025-12-21 01:55:21 +01:00
committed by Bryan Frimin
parent e5c2ce0337
commit 62721f7627
21 changed files with 1974 additions and 498 deletions

View File

@@ -1,39 +0,0 @@
import { graphql } from "relay-runtime";
// import { useTranslate } from "@probo/i18n";
// import { useMutationWithToasts } from "../useMutationWithToasts";
// import type { OrganizationGraphDeleteMutation } from "./__generated__/OrganizationGraphDeleteMutation.graphql";
export const organizationViewQuery = graphql`
query OrganizationGraph_ViewQuery($organizationId: ID!) {
node(id: $organizationId) {
... on Organization {
id
name
...SettingsPageFragment
}
}
}
`;
// const deleteOrganizationMutation = graphql`
// mutation OrganizationGraphDeleteMutation(
// $input: DeleteOrganizationInput!
// $connections: [ID!]!
// ) {
// deleteOrganization(input: $input) {
// deletedOrganizationId @deleteEdge(connections: $connections)
// }
// }
// `;
// export function useDeleteOrganizationMutation() {
// const { __ } = useTranslate();
// return useMutationWithToasts<OrganizationGraphDeleteMutation>(
// deleteOrganizationMutation,
// {
// successMessage: __("Organization deleted successfully."),
// errorMessage: __("Failed to delete organization"),
// }
// );
// }

View File

@@ -1,228 +0,0 @@
/**
* @generated SignedSource<<871b1bf04087566d269a7d5b92353966>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type OrganizationGraph_ViewQuery$variables = {
organizationId: string;
};
export type OrganizationGraph_ViewQuery$data = {
readonly node: {
readonly id?: string;
readonly name?: string;
readonly " $fragmentSpreads": FragmentRefs<"SettingsPageFragment">;
};
};
export type OrganizationGraph_ViewQuery = {
response: OrganizationGraph_ViewQuery$data;
variables: OrganizationGraph_ViewQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "organizationId"
}
],
v1 = [
{
"kind": "Variable",
"name": "id",
"variableName": "organizationId"
}
],
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "OrganizationGraph_ViewQuery",
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"kind": "InlineFragment",
"selections": [
(v2/*: any*/),
(v3/*: any*/),
{
"args": null,
"kind": "FragmentSpread",
"name": "SettingsPageFragment"
}
],
"type": "Organization",
"abstractKey": null
}
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "OrganizationGraph_ViewQuery",
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
(v2/*: any*/),
{
"kind": "InlineFragment",
"selections": [
(v3/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "CustomDomain",
"kind": "LinkedField",
"name": "customDomain",
"plural": false,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "domain",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "sslStatus",
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "DNSRecordInstruction",
"kind": "LinkedField",
"name": "dnsRecords",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "type",
"storageKey": null
},
(v3/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "value",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "ttl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "purpose",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "createdAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "updatedAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "sslExpiresAt",
"storageKey": null
}
],
"storageKey": null
}
],
"type": "Organization",
"abstractKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "a3048b63d657ae43793f9cfca1d0f53d",
"id": null,
"metadata": {},
"name": "OrganizationGraph_ViewQuery",
"operationKind": "query",
"text": "query OrganizationGraph_ViewQuery(\n $organizationId: ID!\n) {\n node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n ...SettingsPageFragment\n }\n id\n }\n}\n\nfragment DomainSettingsTabFragment on Organization {\n id\n customDomain {\n id\n domain\n sslStatus\n dnsRecords {\n type\n name\n value\n ttl\n purpose\n }\n createdAt\n updatedAt\n sslExpiresAt\n }\n}\n\nfragment SettingsPageFragment on Organization {\n id\n name\n ...DomainSettingsTabFragment\n}\n"
}
};
})();
(node as any).hash = "196e8c1fc9c2e0b3c76c8b338ed5c7f7";
export default node;

View File

@@ -0,0 +1,106 @@
import { useTranslate } from "@probo/i18n";
import { Button, Card, IconTrashCan } from "@probo/ui";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
import { useNavigate } from "react-router";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
import { graphql } from "relay-runtime";
import { usePreloadedQuery, type PreloadedQuery } from "react-relay";
import type { GeneralSettingsPageQuery } from "./__generated__/GeneralSettingsPageQuery.graphql";
import { generalSettingsPageQuery } from "./GeneralSettingsPageQuery";
import type { GeneralSettingsPage_deleteMutation } from "./__generated__/GeneralSettingsPage_deleteMutation.graphql";
import { DeleteOrganizationDialog } from "/components/organizations/DeleteOrganizationDialog";
import { OrganizationForm } from "./_components/OrganizationForm";
const deleteOrganizationMutation = graphql`
mutation GeneralSettingsPage_deleteMutation(
$input: DeleteOrganizationInput!
$connections: [ID!]!
) {
deleteOrganization(input: $input) {
deletedOrganizationId @deleteEdge(connections: $connections)
}
}
`;
export function GeneralSettingsPage(props: {
queryRef: PreloadedQuery<GeneralSettingsPageQuery>;
}) {
const { queryRef } = props;
const { __ } = useTranslate();
const navigate = useNavigate();
const { isAuthorized } = use(PermissionsContext);
const canDelete = isAuthorized("Organization", "deleteOrganization");
const { organization } = usePreloadedQuery<GeneralSettingsPageQuery>(
generalSettingsPageQuery,
queryRef,
);
if (organization.__typename === "%other") {
throw new Error("Relay node is not an organization");
}
const [deleteOrganization, isDeletingOrganization] =
useMutationWithToasts<GeneralSettingsPage_deleteMutation>(
deleteOrganizationMutation,
{
successMessage: __("Organization deleted successfully."),
errorMessage: __("Failed to delete organization"),
},
);
const handleDeleteOrganization = () => {
return deleteOrganization({
variables: {
input: {
organizationId: organization.id,
},
connections: [],
},
onSuccess: () => {
navigate("/", { replace: true });
},
});
};
return (
<div className="space-y-6">
<OrganizationForm fKey={organization} />
{canDelete && (
<div className="space-y-4 mt-12">
<h2 className="text-base font-medium text-red-600">
{__("Danger Zone")}
</h2>
<Card padded className="border-red-200 flex items-center gap-3">
<div className="mr-auto">
<h3 className="text-base font-semibold text-red-700">
{__("Delete Organization")}
</h3>
<p className="text-sm text-txt-tertiary">
{__("Permanently delete this organization and all its data.")}{" "}
<span className="text-red-600 font-medium">
{__("This action cannot be undone.")}
</span>
</p>
</div>
<DeleteOrganizationDialog
organizationName={organization.name}
onConfirm={handleDeleteOrganization}
isDeleting={isDeletingOrganization}
>
<Button
variant="danger"
icon={IconTrashCan}
disabled={isDeletingOrganization}
>
{__("Delete Organization")}
</Button>
</DeleteOrganizationDialog>
</Card>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,47 @@
import { graphql } from "relay-runtime";
import { useOrganizationId } from "/hooks/useOrganizationId";
import { useQueryLoader } from "react-relay";
import { useEffect } from "react";
import { GeneralSettingsPage } from "./GeneralSettingsPage";
import type { GeneralSettingsPageQuery } from "./__generated__/GeneralSettingsPageQuery.graphql";
import { IAMRelayProvider } from "/providers/IAMRelayProvider";
export const generalSettingsPageQuery = graphql`
query GeneralSettingsPageQuery($organizationId: ID!) {
organization: node(id: $organizationId) @required(action: THROW) {
__typename
... on Organization {
id
name @required(action: THROW)
...OrganizationFormFragment
}
}
}
`;
function GeneralSettingsPageQuery() {
const organizationId = useOrganizationId();
const [queryRef, loadQuery] = useQueryLoader<GeneralSettingsPageQuery>(
generalSettingsPageQuery,
);
useEffect(() => {
loadQuery({
organizationId,
});
}, [loadQuery, organizationId]);
if (!queryRef) {
return null;
}
return <GeneralSettingsPage queryRef={queryRef} />;
}
export default function () {
return (
<IAMRelayProvider>
<GeneralSettingsPageQuery />
</IAMRelayProvider>
);
}

View File

@@ -0,0 +1,44 @@
import {
IconLock,
IconPeopleAdd,
IconSettingsGear2,
IconStore,
PageHeader,
TabLink,
Tabs,
} from "@probo/ui";
import { Outlet } from "react-router";
import { useOrganizationId } from "/hooks/useOrganizationId";
import { useTranslate } from "@probo/i18n";
export default function SettingsLayout() {
const organizationId = useOrganizationId();
const { __ } = useTranslate();
return (
<div className="space-y-6">
<PageHeader title={__("Settings")} />
<Tabs>
<TabLink to={`/organizations/${organizationId}/settings/general`}>
<IconSettingsGear2 size={20} />
{__("General")}
</TabLink>
<TabLink to={`/organizations/${organizationId}/settings/members`}>
<IconPeopleAdd size={20} />
{__("Members")}
</TabLink>
<TabLink to={`/organizations/${organizationId}/settings/domain`}>
<IconStore size={20} />
{__("Domain")}
</TabLink>
<TabLink to={`/organizations/${organizationId}/settings/saml-sso`}>
<IconLock size={20} />
{__("SAML SSO")}
</TabLink>
</Tabs>
<Outlet />
</div>
);
}

View File

@@ -0,0 +1,198 @@
/**
* @generated SignedSource<<7b9dd98afd5bdc994e7d2e422dec2cf2>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type GeneralSettingsPageQuery$variables = {
organizationId: string;
};
export type GeneralSettingsPageQuery$data = {
readonly organization: {
readonly __typename: "Organization";
readonly id: string;
readonly name: string;
readonly " $fragmentSpreads": FragmentRefs<"OrganizationFormFragment">;
} | {
// This will never be '%other', but we need some
// value in case none of the concrete values match.
readonly __typename: "%other";
};
};
export type GeneralSettingsPageQuery = {
response: GeneralSettingsPageQuery$data;
variables: GeneralSettingsPageQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "organizationId"
}
],
v1 = [
{
"kind": "Variable",
"name": "id",
"variableName": "organizationId"
}
],
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v4 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "GeneralSettingsPageQuery",
"selections": [
{
"kind": "RequiredField",
"field": {
"alias": "organization",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"kind": "InlineFragment",
"selections": [
(v3/*: any*/),
{
"kind": "RequiredField",
"field": (v4/*: any*/),
"action": "THROW"
},
{
"args": null,
"kind": "FragmentSpread",
"name": "OrganizationFormFragment"
}
],
"type": "Organization",
"abstractKey": null
}
],
"storageKey": null
},
"action": "THROW"
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "GeneralSettingsPageQuery",
"selections": [
{
"alias": "organization",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
(v3/*: any*/),
{
"kind": "InlineFragment",
"selections": [
(v4/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "logoUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "horizontalLogoUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "description",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "websiteUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "email",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "headquarterAddress",
"storageKey": null
}
],
"type": "Organization",
"abstractKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "11f654f40301ba539b6d78ead3029308",
"id": null,
"metadata": {},
"name": "GeneralSettingsPageQuery",
"operationKind": "query",
"text": "query GeneralSettingsPageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n ...OrganizationFormFragment\n }\n id\n }\n}\n\nfragment OrganizationFormFragment on Organization {\n id\n name\n logoUrl\n horizontalLogoUrl\n description\n websiteUrl\n email\n headquarterAddress\n}\n"
}
};
})();
(node as any).hash = "a83e24379e0772bbaf19bad92d60c6d4";
export default node;

View File

@@ -0,0 +1,132 @@
/**
* @generated SignedSource<<71619b1be320df410e3ebe2157b11ed4>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type DeleteOrganizationInput = {
organizationId: string;
};
export type GeneralSettingsPage_deleteMutation$variables = {
connections: ReadonlyArray<string>;
input: DeleteOrganizationInput;
};
export type GeneralSettingsPage_deleteMutation$data = {
readonly deleteOrganization: {
readonly deletedOrganizationId: string;
} | null | undefined;
};
export type GeneralSettingsPage_deleteMutation = {
response: GeneralSettingsPage_deleteMutation$data;
variables: GeneralSettingsPage_deleteMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "connections"
},
v1 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
},
v2 = [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "deletedOrganizationId",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": [
(v0/*: any*/),
(v1/*: any*/)
],
"kind": "Fragment",
"metadata": null,
"name": "GeneralSettingsPage_deleteMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "DeleteOrganizationPayload",
"kind": "LinkedField",
"name": "deleteOrganization",
"plural": false,
"selections": [
(v3/*: any*/)
],
"storageKey": null
}
],
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [
(v1/*: any*/),
(v0/*: any*/)
],
"kind": "Operation",
"name": "GeneralSettingsPage_deleteMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "DeleteOrganizationPayload",
"kind": "LinkedField",
"name": "deleteOrganization",
"plural": false,
"selections": [
(v3/*: any*/),
{
"alias": null,
"args": null,
"filters": null,
"handle": "deleteEdge",
"key": "",
"kind": "ScalarHandle",
"name": "deletedOrganizationId",
"handleArgs": [
{
"kind": "Variable",
"name": "connections",
"variableName": "connections"
}
]
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "53d34f4a9fe61e454f0ca80e8f34d88a",
"id": null,
"metadata": {},
"name": "GeneralSettingsPage_deleteMutation",
"operationKind": "mutation",
"text": "mutation GeneralSettingsPage_deleteMutation(\n $input: DeleteOrganizationInput!\n) {\n deleteOrganization(input: $input) {\n deletedOrganizationId\n }\n}\n"
}
};
})();
(node as any).hash = "c473475817881aaa4adee8f183faf580";
export default node;

View File

@@ -0,0 +1,373 @@
import {
Avatar,
Button,
Card,
Dialog,
DialogContent,
DialogFooter,
Field,
FileButton,
IconTrashCan,
Label,
Spinner,
Textarea,
useDialogRef,
} from "@probo/ui";
import { use, useState, type ChangeEventHandler } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
import { useFormWithSchema } from "/hooks/useFormWithSchema";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
import { graphql } from "relay-runtime";
import z from "zod";
import { useTranslate } from "@probo/i18n";
import { useFragment } from "react-relay";
import type { OrganizationFormFragment$key } from "./__generated__/OrganizationFormFragment.graphql";
const fragment = graphql`
fragment OrganizationFormFragment on Organization {
id
name @required(action: THROW)
logoUrl
horizontalLogoUrl
description
websiteUrl
email
headquarterAddress
}
`;
const updateOrganizationMutation = graphql`
mutation OrganizationForm_updateMutation($input: UpdateOrganizationInput!) {
updateOrganization(input: $input) {
organization {
id
name
logoUrl
horizontalLogoUrl
description
websiteUrl
email
headquarterAddress
}
}
}
`;
const deleteHorizontalLogoMutation = graphql`
mutation OrganizationForm_deleteHorizontalLogoMutation(
$input: DeleteOrganizationHorizontalLogoInput!
) {
deleteOrganizationHorizontalLogo(input: $input) {
organization {
id
horizontalLogoUrl
}
}
}
`;
const organizationSchema = z.object({
name: z.string().min(1, "Organization name is required"),
description: z.string().optional(),
websiteUrl: z.string().optional(),
email: z.string().optional(),
headquarterAddress: z.string().optional(),
});
type OrganizationFormData = z.infer<typeof organizationSchema>;
export function OrganizationForm(props: {
fKey: OrganizationFormFragment$key;
}) {
const { fKey } = props;
const { __ } = useTranslate();
const deleteDialogRef = useDialogRef();
const { isAuthorized } = use(PermissionsContext);
const canUpdate = isAuthorized("Organization", "updateOrganization");
const [logoPreview, setLogoPreview] = useState<string | null>(null);
const [horizontalLogoPreview, setHorizontalLogoPreview] = useState<
string | null
>(null);
const organization = useFragment<OrganizationFormFragment$key>(
fragment,
fKey,
);
const [updateOrganization, isUpdatingOrganization] = useMutationWithToasts(
updateOrganizationMutation,
{
successMessage: __("Organization updated successfully"),
errorMessage: __("Failed to update organization"),
},
);
const [deleteHorizontalLogo, isDeletingHorizontalLogo] =
useMutationWithToasts(deleteHorizontalLogoMutation, {
successMessage: __("Horizontal logo deleted successfully"),
errorMessage: __("Failed to delete horizontal logo"),
});
const { formState, handleSubmit, register } = useFormWithSchema(
organizationSchema,
{
defaultValues: {
name: organization.name,
description: organization.description || "",
websiteUrl: organization.websiteUrl || "",
email: organization.email || "",
headquarterAddress: organization.headquarterAddress || "",
},
},
);
const handleLogoChange: ChangeEventHandler<HTMLInputElement> = (e) => {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
setLogoPreview(reader.result as string);
};
reader.readAsDataURL(file);
updateOrganization({
variables: {
input: {
organizationId: organization.id,
logoFile: null,
},
},
uploadables: {
"input.logoFile": file,
},
onCompleted: () => {
setLogoPreview(null);
},
});
};
const handleHorizontalLogoChange: ChangeEventHandler<HTMLInputElement> = (
e,
) => {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
setHorizontalLogoPreview(reader.result as string);
};
reader.readAsDataURL(file);
updateOrganization({
variables: {
input: {
organizationId: organization.id,
horizontalLogoFile: null,
},
},
uploadables: {
"input.horizontalLogoFile": file,
},
onCompleted: () => {
setHorizontalLogoPreview(null);
},
});
};
const handleDeleteHorizontalLogo = () => {
deleteHorizontalLogo({
variables: {
input: {
organizationId: organization.id,
},
},
onCompleted: () => {
deleteDialogRef.current?.close();
},
});
};
const onSubmit = handleSubmit((data: OrganizationFormData) => {
updateOrganization({
variables: {
input: {
organizationId: organization.id,
name: data.name,
description: data.description || null,
websiteUrl: data.websiteUrl || null,
email: data.email || null,
headquarterAddress: data.headquarterAddress || null,
},
},
});
});
return (
<form onSubmit={onSubmit} className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-base font-medium">{__("Organization details")}</h2>
{formState.isSubmitting && <Spinner />}
</div>
<Card padded className="space-y-4">
<div>
<Label>{__("Organization logo")}</Label>
<div className="flex w-max items-center gap-4">
<Avatar
src={logoPreview || organization.logoUrl}
name={organization.name}
size="xl"
/>
{canUpdate && (
<FileButton
disabled={formState.isSubmitting || isUpdatingOrganization}
onChange={handleLogoChange}
variant="secondary"
className="ml-auto"
accept="image/png,image/jpeg,image/jpg,image/svg+xml"
>
{isUpdatingOrganization
? __("Uploading...")
: __("Change logo")}
</FileButton>
)}
</div>
</div>
<div>
<Label>{__("Horizontal logo")}</Label>
<p className="text-sm text-txt-tertiary mb-2">
{__(
"Upload a horizontal version of your logo for use in documents",
)}
</p>
<div className="flex items-center gap-4">
{(horizontalLogoPreview || organization.horizontalLogoUrl) && (
<div className="border border-border-solid rounded-md p-4 bg-surface-secondary">
<img
src={
horizontalLogoPreview ||
organization.horizontalLogoUrl ||
undefined
}
alt={__("Horizontal logo")}
className="h-12 max-w-xs object-contain"
/>
</div>
)}
{canUpdate && (
<FileButton
disabled={formState.isSubmitting || isUpdatingOrganization}
onChange={handleHorizontalLogoChange}
variant="secondary"
accept="image/png,image/jpeg,image/jpg,image/svg+xml"
>
{isUpdatingOrganization
? __("Uploading...")
: horizontalLogoPreview || organization.horizontalLogoUrl
? __("Change horizontal logo")
: __("Upload horizontal logo")}
</FileButton>
)}
{canUpdate && organization.horizontalLogoUrl && (
<Dialog
ref={deleteDialogRef}
trigger={
<Button
type="button"
variant="quaternary"
icon={IconTrashCan}
aria-label={__("Delete horizontal logo")}
className="text-red-600 hover:text-red-700"
/>
}
title={__("Delete Horizontal Logo")}
className="max-w-md"
>
<DialogContent padded>
<p className="text-txt-secondary">
{__("Are you sure you want to delete the horizontal logo?")}
</p>
<p className="text-txt-secondary mt-2">
{__("This action cannot be undone.")}
</p>
</DialogContent>
<DialogFooter>
<Button
variant="danger"
onClick={handleDeleteHorizontalLogo}
disabled={isDeletingHorizontalLogo}
icon={isDeletingHorizontalLogo ? Spinner : IconTrashCan}
>
{isDeletingHorizontalLogo
? __("Deleting...")
: __("Delete")}
</Button>
</DialogFooter>
</Dialog>
)}
</div>
</div>
<Field
{...register("name")}
readOnly={formState.isSubmitting || !canUpdate}
name="name"
type="text"
label={__("Organization name")}
placeholder={__("Organization name")}
/>
<div>
<Label>{__("Description")}</Label>
<Textarea
{...register("description")}
readOnly={formState.isSubmitting || !canUpdate}
name="description"
placeholder={__("Brief description of your organization")}
rows={3}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Field
{...register("websiteUrl")}
readOnly={formState.isSubmitting || !canUpdate}
name="websiteUrl"
type="url"
label={__("Website URL")}
placeholder={__("https://example.com")}
/>
<Field
{...register("email")}
readOnly={formState.isSubmitting || !canUpdate}
name="email"
type="email"
label={__("Email")}
placeholder={__("contact@example.com")}
/>
</div>
<div>
<Label>{__("Headquarter Address")}</Label>
<Textarea
{...register("headquarterAddress")}
readOnly={formState.isSubmitting || !canUpdate}
name="headquarterAddress"
placeholder={__("123 Main St, City, Country")}
/>
</div>
{formState.isDirty && canUpdate && (
<div className="flex justify-end pt-6">
<Button
type="submit"
disabled={formState.isSubmitting || isUpdatingOrganization}
>
{formState.isSubmitting || isUpdatingOrganization
? __("Updating...")
: __("Update Organization")}
</Button>
</div>
)}
</Card>
</form>
);
}

View File

@@ -0,0 +1,102 @@
/**
* @generated SignedSource<<8deb33fbc8305caa3d3de954cf555c79>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type OrganizationFormFragment$data = {
readonly description: string | null | undefined;
readonly email: string | null | undefined;
readonly headquarterAddress: string | null | undefined;
readonly horizontalLogoUrl: string | null | undefined;
readonly id: string;
readonly logoUrl: string | null | undefined;
readonly name: string;
readonly websiteUrl: string | null | undefined;
readonly " $fragmentType": "OrganizationFormFragment";
};
export type OrganizationFormFragment$key = {
readonly " $data"?: OrganizationFormFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"OrganizationFormFragment">;
};
const node: ReaderFragment = {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "OrganizationFormFragment",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"kind": "RequiredField",
"field": {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
"action": "THROW"
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "logoUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "horizontalLogoUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "description",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "websiteUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "email",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "headquarterAddress",
"storageKey": null
}
],
"type": "Organization",
"abstractKey": null
};
(node as any).hash = "0894093a550e5ba033a33b255b1bf2c7";
export default node;

View File

@@ -0,0 +1,113 @@
/**
* @generated SignedSource<<ea00a00b77edf3939a310de2b0f7b8e0>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type DeleteOrganizationHorizontalLogoInput = {
organizationId: string;
};
export type OrganizationForm_deleteHorizontalLogoMutation$variables = {
input: DeleteOrganizationHorizontalLogoInput;
};
export type OrganizationForm_deleteHorizontalLogoMutation$data = {
readonly deleteOrganizationHorizontalLogo: {
readonly organization: {
readonly horizontalLogoUrl: string | null | undefined;
readonly id: string;
};
} | null | undefined;
};
export type OrganizationForm_deleteHorizontalLogoMutation = {
response: OrganizationForm_deleteHorizontalLogoMutation$data;
variables: OrganizationForm_deleteHorizontalLogoMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
}
],
v1 = [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
"concreteType": "DeleteOrganizationHorizontalLogoPayload",
"kind": "LinkedField",
"name": "deleteOrganizationHorizontalLogo",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Organization",
"kind": "LinkedField",
"name": "organization",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "horizontalLogoUrl",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "OrganizationForm_deleteHorizontalLogoMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "OrganizationForm_deleteHorizontalLogoMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "2ec98faaaf8317f642f5db54c7bbed80",
"id": null,
"metadata": {},
"name": "OrganizationForm_deleteHorizontalLogoMutation",
"operationKind": "mutation",
"text": "mutation OrganizationForm_deleteHorizontalLogoMutation(\n $input: DeleteOrganizationHorizontalLogoInput!\n) {\n deleteOrganizationHorizontalLogo(input: $input) {\n organization {\n id\n horizontalLogoUrl\n }\n }\n}\n"
}
};
})();
(node as any).hash = "9ea67e345e506fef9d85ee31bc360d51";
export default node;

View File

@@ -0,0 +1,168 @@
/**
* @generated SignedSource<<9602c1a0ad55d4889431c8e5f54f4461>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type UpdateOrganizationInput = {
description?: string | null | undefined;
email?: string | null | undefined;
headquarterAddress?: string | null | undefined;
horizontalLogoFile?: any | null | undefined;
logoFile?: any | null | undefined;
name?: string | null | undefined;
organizationId: string;
websiteUrl?: string | null | undefined;
};
export type OrganizationForm_updateMutation$variables = {
input: UpdateOrganizationInput;
};
export type OrganizationForm_updateMutation$data = {
readonly updateOrganization: {
readonly organization: {
readonly description: string | null | undefined;
readonly email: string | null | undefined;
readonly headquarterAddress: string | null | undefined;
readonly horizontalLogoUrl: string | null | undefined;
readonly id: string;
readonly logoUrl: string | null | undefined;
readonly name: string;
readonly websiteUrl: string | null | undefined;
} | null | undefined;
} | null | undefined;
};
export type OrganizationForm_updateMutation = {
response: OrganizationForm_updateMutation$data;
variables: OrganizationForm_updateMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
}
],
v1 = [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
"concreteType": "UpdateOrganizationPayload",
"kind": "LinkedField",
"name": "updateOrganization",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Organization",
"kind": "LinkedField",
"name": "organization",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "logoUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "horizontalLogoUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "description",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "websiteUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "email",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "headquarterAddress",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "OrganizationForm_updateMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "OrganizationForm_updateMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "f2a249d13ff38645cb858b494c05c59d",
"id": null,
"metadata": {},
"name": "OrganizationForm_updateMutation",
"operationKind": "mutation",
"text": "mutation OrganizationForm_updateMutation(\n $input: UpdateOrganizationInput!\n) {\n updateOrganization(input: $input) {\n organization {\n id\n name\n logoUrl\n horizontalLogoUrl\n description\n websiteUrl\n email\n headquarterAddress\n }\n }\n}\n"
}
};
})();
(node as any).hash = "c4dd7a42f611fa210594017a5a63d159";
export default node;

View File

@@ -1,75 +0,0 @@
import { Outlet } from "react-router";
import { useTranslate } from "@probo/i18n";
import type { PreloadedQuery } from "react-relay";
import type { OrganizationGraph_ViewQuery } from "/hooks/graph/__generated__/OrganizationGraph_ViewQuery.graphql";
import { usePreloadedQuery } from "react-relay";
import { organizationViewQuery } from "/hooks/graph/OrganizationGraph";
import {
IconSettingsGear2,
IconPeopleAdd,
IconStore,
IconLock,
PageHeader,
TabLink,
Tabs,
} from "@probo/ui";
import { useOrganizationId } from "/hooks/useOrganizationId";
import { graphql } from "relay-runtime";
import type { SettingsPageFragment$key } from "./__generated__/SettingsPageFragment.graphql";
import { useFragment } from "react-relay";
const organizationFragment = graphql`
fragment SettingsPageFragment on Organization {
id
name
# ...GeneralSettingsTabFragment
# ...MembersSettingsTabMembershipsFragment
# ...MembersSettingsTabInvitationsFragment
...DomainSettingsTabFragment
# ...SAMLSettingsTabFragment
}
`;
type Props = {
queryRef: PreloadedQuery<OrganizationGraph_ViewQuery>;
};
export default function SettingsPage({ queryRef }: Props) {
const { __ } = useTranslate();
const organizationId = useOrganizationId();
const organizationKey = usePreloadedQuery(
organizationViewQuery,
queryRef
).node;
const organization = useFragment<SettingsPageFragment$key>(
organizationFragment,
organizationKey
);
return (
<div className="space-y-6">
<PageHeader title={__("Settings")} />
<Tabs>
<TabLink to={`/organizations/${organizationId}/settings/general`}>
<IconSettingsGear2 size={20} />
{__("General")}
</TabLink>
<TabLink to={`/organizations/${organizationId}/settings/members`}>
<IconPeopleAdd size={20} />
{__("Members")}
</TabLink>
<TabLink to={`/organizations/${organizationId}/settings/domain`}>
<IconStore size={20} />
{__("Domain")}
</TabLink>
<TabLink to={`/organizations/${organizationId}/settings/saml-sso`}>
<IconLock size={20} />
{__("SAML SSO")}
</TabLink>
</Tabs>
<Outlet context={{ organization }} />
</div>
);
}

View File

@@ -1,56 +0,0 @@
/**
* @generated SignedSource<<1b79ba90084eae4139b4fd4a31e5061b>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type SettingsPageFragment$data = {
readonly id: string;
readonly name: string;
readonly " $fragmentSpreads": FragmentRefs<"DomainSettingsTabFragment">;
readonly " $fragmentType": "SettingsPageFragment";
};
export type SettingsPageFragment$key = {
readonly " $data"?: SettingsPageFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"SettingsPageFragment">;
};
const node: ReaderFragment = {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "SettingsPageFragment",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"args": null,
"kind": "FragmentSpread",
"name": "DomainSettingsTabFragment"
}
],
"type": "Organization",
"abstractKey": null
};
(node as any).hash = "c00c2edf8bd9f8255c6bd6943bd4d445";
export default node;

View File

@@ -12,7 +12,6 @@ import { riskRoutes } from "./routes/riskRoutes.ts";
import { measureRoutes } from "./routes/measureRoutes.ts";
import { documentsRoutes } from "./routes/documentsRoutes.ts";
import { vendorRoutes } from "./routes/vendorRoutes.ts";
import { organizationViewQuery } from "./hooks/graph/OrganizationGraph.ts";
import { peopleRoutes } from "./routes/peopleRoutes.ts";
import { frameworkRoutes } from "./routes/frameworkRoutes.ts";
import { PageError } from "./components/PageError.tsx";
@@ -209,27 +208,23 @@ const routes = [
{
path: "settings",
Fallback: PageSkeleton,
loader: loaderFromQueryLoader(({ organizationId }) =>
loadQuery(coreEnvironment, organizationViewQuery, {
organizationId: organizationId!,
}),
),
Component: withQueryRef(
lazy(() => import("./pages/organizations/SettingsPage")),
Component: lazy(
() => import("./pages/iam/organizations/settings/SettingsLayout.tsx"),
),
children: [
{
path: "",
index: true,
loader: () => {
throw redirect("general");
},
},
// {
// path: "general",
// Component: lazy(
// () => import("./pages/organizations/settings/GeneralSettingsTab")
// ),
// },
{
path: "general",
Component: lazy(
() =>
import("./pages/iam/organizations/settings/GeneralSettingsPageQuery.tsx"),
),
},
// {
// path: "members",
// Component: lazy(

View File

@@ -58,8 +58,12 @@ type (
UpdateOrganizationRequest struct {
Name *string
LogoFile **UploadedFile
HorizontalLogoFile **UploadedFile
LogoFile *UploadedFile
HorizontalLogoFile *UploadedFile
Description **string
WebsiteURL **string
Email **string
HeadquarterAddress **string
}
CreateSAMLConfigurationRequest struct {
@@ -91,6 +95,8 @@ type (
const (
TokenTypeAPIKey = "api_key"
ContentMaxLength = 5000
DefaultAttributeEmail = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
DefaultAttributeFirstname = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"
DefaultAttributeLastname = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"
@@ -124,22 +130,24 @@ func (req UpdateOrganizationRequest) Validate() error {
v := validator.New()
fv := filevalidation.NewValidator(filevalidation.WithCategories(filevalidation.CategoryImage))
if req.LogoFile != nil && *req.LogoFile != nil {
err := fv.Validate((**req.LogoFile).Filename, (**req.LogoFile).ContentType, (**req.LogoFile).Size)
if err != nil {
v.Check(req.Name, "name", validator.SafeTextNoNewLine(255))
v.Check(req.Description, "description", validator.SafeText(ContentMaxLength))
v.Check(req.WebsiteURL, "website_url", validator.SafeText(2048))
v.Check(req.Email, "email", validator.SafeText(255))
v.Check(req.HeadquarterAddress, "headquarter_address", validator.SafeText(2048))
v.Check(req.LogoFile, "logo_file", validator.NotEmpty())
if req.LogoFile != nil {
if err := fv.Validate(req.LogoFile.Filename, req.LogoFile.ContentType, req.LogoFile.Size); err != nil {
return fmt.Errorf("invalid logo file: %w", err)
}
}
if req.HorizontalLogoFile != nil && *req.HorizontalLogoFile != nil {
err := fv.Validate((**req.HorizontalLogoFile).Filename, (**req.HorizontalLogoFile).ContentType, (**req.HorizontalLogoFile).Size)
if err != nil {
v.Check(req.HorizontalLogoFile, "horizontal_logo_file", validator.NotEmpty())
if req.HorizontalLogoFile != nil {
if err := fv.Validate(req.HorizontalLogoFile.Filename, req.HorizontalLogoFile.ContentType, req.HorizontalLogoFile.Size); err != nil {
return fmt.Errorf("invalid horizontal logo file: %w", err)
}
}
v.Check(req.Name, "name", validator.Required(), validator.SafeTextNoNewLine(255))
return v.Error()
}
@@ -543,12 +551,12 @@ func (s *OrganizationService) UpdateOrganization(ctx context.Context, organizati
// TODO: s3 upload happen before we validate the tenantID
if req.LogoFile != nil && *req.LogoFile != nil {
if req.LogoFile != nil {
var (
fileID = gid.New(tenantID, coredata.FileEntityType)
objectKey = uuid.MustNewV7()
filename = (**req.LogoFile).Filename
contentType = (**req.LogoFile).ContentType
filename = (*req.LogoFile).Filename
contentType = (*req.LogoFile).ContentType
)
logoFile = &coredata.File{
@@ -557,7 +565,7 @@ func (s *OrganizationService) UpdateOrganization(ctx context.Context, organizati
MimeType: contentType,
FileName: filename,
FileKey: objectKey.String(),
FileSize: (**req.LogoFile).Size,
FileSize: (*req.LogoFile).Size,
CreatedAt: now,
UpdatedAt: now,
}
@@ -565,7 +573,7 @@ func (s *OrganizationService) UpdateOrganization(ctx context.Context, organizati
fileSize, err := s.fm.PutFile(
ctx,
logoFile,
(**req.LogoFile).Content,
(*req.LogoFile).Content,
map[string]string{
"file-id": fileID.String(),
"organization-id": organizationID.String(),
@@ -579,12 +587,12 @@ func (s *OrganizationService) UpdateOrganization(ctx context.Context, organizati
logoFile.FileSize = fileSize
}
if req.HorizontalLogoFile != nil && *req.HorizontalLogoFile != nil {
if req.HorizontalLogoFile != nil {
var (
fileID = gid.New(tenantID, coredata.FileEntityType)
objectKey = uuid.MustNewV7()
filename = (**req.HorizontalLogoFile).Filename
contentType = (**req.HorizontalLogoFile).ContentType
filename = (*req.HorizontalLogoFile).Filename
contentType = (*req.HorizontalLogoFile).ContentType
now = time.Now()
)
@@ -594,7 +602,7 @@ func (s *OrganizationService) UpdateOrganization(ctx context.Context, organizati
MimeType: contentType,
FileName: filename,
FileKey: objectKey.String(),
FileSize: (**req.HorizontalLogoFile).Size,
FileSize: (*req.HorizontalLogoFile).Size,
CreatedAt: now,
UpdatedAt: now,
}
@@ -602,7 +610,7 @@ func (s *OrganizationService) UpdateOrganization(ctx context.Context, organizati
fileSize, err := s.fm.PutFile(
ctx,
horizontalLogoFile,
(**req.HorizontalLogoFile).Content,
(*req.HorizontalLogoFile).Content,
map[string]string{
"file-id": fileID.String(),
"organization-id": organizationID.String(),
@@ -630,30 +638,46 @@ func (s *OrganizationService) UpdateOrganization(ctx context.Context, organizati
organization.Name = *req.Name
}
if req.LogoFile != nil {
if *req.LogoFile != nil {
err := logoFile.Insert(ctx, tx, scope)
if err != nil {
return fmt.Errorf("cannot insert file: %w", err)
}
organization.LogoFileID = &logoFile.ID
} else {
organization.LogoFileID = nil
}
if req.Name != nil {
organization.Name = *req.Name
}
if req.HorizontalLogoFile != nil {
if *req.HorizontalLogoFile != nil {
err := horizontalLogoFile.Insert(ctx, tx, scope)
if err != nil {
return fmt.Errorf("cannot insert file: %w", err)
if req.Description != nil {
organization.Description = *req.Description
}
if req.WebsiteURL != nil {
organization.WebsiteURL = *req.WebsiteURL
}
if req.Email != nil {
if *req.Email != nil {
if _, err := mail.ParseAddr(**req.Email); err != nil {
return fmt.Errorf("invalid email address: %w", err)
}
organization.HorizontalLogoFileID = &horizontalLogoFile.ID
} else {
organization.HorizontalLogoFileID = nil
}
organization.Email = *req.Email
}
if req.HeadquarterAddress != nil {
organization.HeadquarterAddress = *req.HeadquarterAddress
}
if logoFile != nil {
err := logoFile.Insert(ctx, tx, scope)
if err != nil {
return fmt.Errorf("cannot insert file: %w", err)
}
organization.LogoFileID = &logoFile.ID
}
if horizontalLogoFile != nil {
err := horizontalLogoFile.Insert(ctx, tx, scope)
if err != nil {
return fmt.Errorf("cannot insert file: %w", err)
}
organization.HorizontalLogoFileID = &horizontalLogoFile.ID
}
err = organization.Update(ctx, scope, tx)

View File

@@ -19,6 +19,7 @@ package connect_v1
import (
"time"
"github.com/99designs/gqlgen/graphql"
"github.com/go-chi/chi/v5"
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/baseurl"
@@ -65,3 +66,11 @@ func NewMux(logger *log.Logger, svc *iam.Service, cookieConfig securecookie.Conf
return r
}
func UnwrapOmittable[T any](field graphql.Omittable[T]) *T {
if !field.IsSet() {
return nil
}
value := field.Value()
return &value
}

View File

@@ -106,6 +106,9 @@ type Mutation {
deleteOrganization(
input: DeleteOrganizationInput!
): DeleteOrganizationPayload @session(required: PRESENT)
deleteOrganizationHorizontalLogo(
input: DeleteOrganizationHorizontalLogoInput!
): DeleteOrganizationHorizontalLogoPayload @session(required: PRESENT)
inviteMember(input: InviteMemberInput!): InviteMemberPayload
@session(required: PRESENT)
@@ -200,6 +203,10 @@ type Organization implements Node {
name: String!
logoUrl: String @goField(forceResolver: true)
horizontalLogoUrl: String @goField(forceResolver: true)
email: String
description: String
websiteUrl: String
headquarterAddress: String
createdAt: Datetime!
updatedAt: Datetime!
@@ -641,14 +648,22 @@ input CreateOrganizationInput {
input UpdateOrganizationInput {
organizationId: ID!
name: String
logoFile: Upload @goField(omittable: true)
horizontalLogoFile: Upload @goField(omittable: true)
logoFile: Upload
horizontalLogoFile: Upload
description: String @goField(omittable: true)
websiteUrl: String @goField(omittable: true)
email: String @goField(omittable: true)
headquarterAddress: String @goField(omittable: true)
}
input DeleteOrganizationInput {
organizationId: ID!
}
input DeleteOrganizationHorizontalLogoInput {
organizationId: ID!
}
input SessionPolicyInput {
maxSessionDurationHours: Int
idleTimeoutMinutes: Int
@@ -825,6 +840,10 @@ type DeleteOrganizationPayload {
deletedOrganizationId: ID!
}
type DeleteOrganizationHorizontalLogoPayload {
organization: Organization!
}
type InviteMemberPayload {
invitationEdge: InvitationEdge!
}

View File

@@ -119,6 +119,10 @@ type ComplexityRoot struct {
DeletedInvitationID func(childComplexity int) int
}
DeleteOrganizationHorizontalLogoPayload struct {
Organization func(childComplexity int) int
}
DeleteOrganizationPayload struct {
DeletedOrganizationID func(childComplexity int) int
}
@@ -218,37 +222,41 @@ type ComplexityRoot struct {
}
Mutation struct {
AcceptInvitation func(childComplexity int, input types.AcceptInvitationInput) int
AssumeOrganizationSession func(childComplexity int, input types.AssumeOrganizationSessionInput) int
ChangeEmail func(childComplexity int, input types.ChangeEmailInput) int
ChangePassword func(childComplexity int, input types.ChangePasswordInput) int
CreateOrganization func(childComplexity int, input types.CreateOrganizationInput) int
CreatePersonalAPIKey func(childComplexity int, input types.CreatePersonalAPIKeyInput) int
CreateSAMLConfiguration func(childComplexity int, input types.CreateSAMLConfigurationInput) int
DeleteInvitation func(childComplexity int, input types.DeleteInvitationInput) int
DeleteOrganization func(childComplexity int, input types.DeleteOrganizationInput) int
DeleteSAMLConfiguration func(childComplexity int, input types.DeleteSAMLConfigurationInput) int
ForgotPassword func(childComplexity int, input types.ForgotPasswordInput) int
InviteMember func(childComplexity int, input types.InviteMemberInput) int
RemoveMember func(childComplexity int, input types.RemoveMemberInput) int
ResetPassword func(childComplexity int, input types.ResetPasswordInput) int
RevokeAllSessions func(childComplexity int) int
RevokePersonalAPIKey func(childComplexity int, input types.RevokePersonalAPIKeyInput) int
RevokeSession func(childComplexity int, input types.RevokeSessionInput) int
SignIn func(childComplexity int, input types.SignInInput) int
SignOut func(childComplexity int) int
SignUp func(childComplexity int, input types.SignUpInput) int
SignUpFromInvitation func(childComplexity int, input types.SignUpFromInvitationInput) int
UpdateIdentityProfile func(childComplexity int, input types.UpdateIdentityProfileInput) int
UpdateOrganization func(childComplexity int, input types.UpdateOrganizationInput) int
UpdatePersonalAPIKey func(childComplexity int, input types.UpdatePersonalAPIKeyInput) int
UpdateSAMLConfiguration func(childComplexity int, input types.UpdateSAMLConfigurationInput) int
VerifyEmail func(childComplexity int, input types.VerifyEmailInput) int
AcceptInvitation func(childComplexity int, input types.AcceptInvitationInput) int
AssumeOrganizationSession func(childComplexity int, input types.AssumeOrganizationSessionInput) int
ChangeEmail func(childComplexity int, input types.ChangeEmailInput) int
ChangePassword func(childComplexity int, input types.ChangePasswordInput) int
CreateOrganization func(childComplexity int, input types.CreateOrganizationInput) int
CreatePersonalAPIKey func(childComplexity int, input types.CreatePersonalAPIKeyInput) int
CreateSAMLConfiguration func(childComplexity int, input types.CreateSAMLConfigurationInput) int
DeleteInvitation func(childComplexity int, input types.DeleteInvitationInput) int
DeleteOrganization func(childComplexity int, input types.DeleteOrganizationInput) int
DeleteOrganizationHorizontalLogo func(childComplexity int, input types.DeleteOrganizationHorizontalLogoInput) int
DeleteSAMLConfiguration func(childComplexity int, input types.DeleteSAMLConfigurationInput) int
ForgotPassword func(childComplexity int, input types.ForgotPasswordInput) int
InviteMember func(childComplexity int, input types.InviteMemberInput) int
RemoveMember func(childComplexity int, input types.RemoveMemberInput) int
ResetPassword func(childComplexity int, input types.ResetPasswordInput) int
RevokeAllSessions func(childComplexity int) int
RevokePersonalAPIKey func(childComplexity int, input types.RevokePersonalAPIKeyInput) int
RevokeSession func(childComplexity int, input types.RevokeSessionInput) int
SignIn func(childComplexity int, input types.SignInInput) int
SignOut func(childComplexity int) int
SignUp func(childComplexity int, input types.SignUpInput) int
SignUpFromInvitation func(childComplexity int, input types.SignUpFromInvitationInput) int
UpdateIdentityProfile func(childComplexity int, input types.UpdateIdentityProfileInput) int
UpdateOrganization func(childComplexity int, input types.UpdateOrganizationInput) int
UpdatePersonalAPIKey func(childComplexity int, input types.UpdatePersonalAPIKeyInput) int
UpdateSAMLConfiguration func(childComplexity int, input types.UpdateSAMLConfigurationInput) int
VerifyEmail func(childComplexity int, input types.VerifyEmailInput) int
}
Organization struct {
AvailableApplications func(childComplexity int) int
CreatedAt func(childComplexity int) int
Description func(childComplexity int) int
Email func(childComplexity int) int
HeadquarterAddress func(childComplexity int) int
HorizontalLogoURL func(childComplexity int) int
ID func(childComplexity int) int
Invitations func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, status *coredata.InvitationStatus) int
@@ -257,6 +265,7 @@ type ComplexityRoot struct {
Name func(childComplexity int) int
SamlConfigurations func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int
UpdatedAt func(childComplexity int) int
WebsiteURL func(childComplexity int) int
}
OrganizationSessionCreated struct {
@@ -491,6 +500,7 @@ type MutationResolver interface {
CreateOrganization(ctx context.Context, input types.CreateOrganizationInput) (*types.CreateOrganizationPayload, error)
UpdateOrganization(ctx context.Context, input types.UpdateOrganizationInput) (*types.UpdateOrganizationPayload, error)
DeleteOrganization(ctx context.Context, input types.DeleteOrganizationInput) (*types.DeleteOrganizationPayload, error)
DeleteOrganizationHorizontalLogo(ctx context.Context, input types.DeleteOrganizationHorizontalLogoInput) (*types.DeleteOrganizationHorizontalLogoPayload, error)
InviteMember(ctx context.Context, input types.InviteMemberInput) (*types.InviteMemberPayload, error)
DeleteInvitation(ctx context.Context, input types.DeleteInvitationInput) (*types.DeleteInvitationPayload, error)
RemoveMember(ctx context.Context, input types.RemoveMemberInput) (*types.RemoveMemberPayload, error)
@@ -661,6 +671,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.DeleteInvitationPayload.DeletedInvitationID(childComplexity), true
case "DeleteOrganizationHorizontalLogoPayload.organization":
if e.complexity.DeleteOrganizationHorizontalLogoPayload.Organization == nil {
break
}
return e.complexity.DeleteOrganizationHorizontalLogoPayload.Organization(childComplexity), true
case "DeleteOrganizationPayload.deletedOrganizationId":
if e.complexity.DeleteOrganizationPayload.DeletedOrganizationID == nil {
break
@@ -1169,6 +1186,17 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
}
return e.complexity.Mutation.DeleteOrganization(childComplexity, args["input"].(types.DeleteOrganizationInput)), true
case "Mutation.deleteOrganizationHorizontalLogo":
if e.complexity.Mutation.DeleteOrganizationHorizontalLogo == nil {
break
}
args, err := ec.field_Mutation_deleteOrganizationHorizontalLogo_args(ctx, rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.DeleteOrganizationHorizontalLogo(childComplexity, args["input"].(types.DeleteOrganizationHorizontalLogoInput)), true
case "Mutation.deleteSAMLConfiguration":
if e.complexity.Mutation.DeleteSAMLConfiguration == nil {
break
@@ -1359,6 +1387,24 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
}
return e.complexity.Organization.CreatedAt(childComplexity), true
case "Organization.description":
if e.complexity.Organization.Description == nil {
break
}
return e.complexity.Organization.Description(childComplexity), true
case "Organization.email":
if e.complexity.Organization.Email == nil {
break
}
return e.complexity.Organization.Email(childComplexity), true
case "Organization.headquarterAddress":
if e.complexity.Organization.HeadquarterAddress == nil {
break
}
return e.complexity.Organization.HeadquarterAddress(childComplexity), true
case "Organization.horizontalLogoUrl":
if e.complexity.Organization.HorizontalLogoURL == nil {
break
@@ -1422,6 +1468,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
}
return e.complexity.Organization.UpdatedAt(childComplexity), true
case "Organization.websiteUrl":
if e.complexity.Organization.WebsiteURL == nil {
break
}
return e.complexity.Organization.WebsiteURL(childComplexity), true
case "OrganizationSessionCreated.membership":
if e.complexity.OrganizationSessionCreated.Membership == nil {
@@ -2031,6 +2083,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
ec.unmarshalInputDeactivateAccountInput,
ec.unmarshalInputDeleteAccountInput,
ec.unmarshalInputDeleteInvitationInput,
ec.unmarshalInputDeleteOrganizationHorizontalLogoInput,
ec.unmarshalInputDeleteOrganizationInput,
ec.unmarshalInputDeleteSAMLConfigurationInput,
ec.unmarshalInputForgotPasswordInput,
@@ -2258,6 +2311,9 @@ type Mutation {
deleteOrganization(
input: DeleteOrganizationInput!
): DeleteOrganizationPayload @session(required: PRESENT)
deleteOrganizationHorizontalLogo(
input: DeleteOrganizationHorizontalLogoInput!
): DeleteOrganizationHorizontalLogoPayload @session(required: PRESENT)
inviteMember(input: InviteMemberInput!): InviteMemberPayload
@session(required: PRESENT)
@@ -2352,6 +2408,10 @@ type Organization implements Node {
name: String!
logoUrl: String @goField(forceResolver: true)
horizontalLogoUrl: String @goField(forceResolver: true)
email: String
description: String
websiteUrl: String
headquarterAddress: String
createdAt: Datetime!
updatedAt: Datetime!
@@ -2793,14 +2853,22 @@ input CreateOrganizationInput {
input UpdateOrganizationInput {
organizationId: ID!
name: String
logoFile: Upload @goField(omittable: true)
horizontalLogoFile: Upload @goField(omittable: true)
logoFile: Upload
horizontalLogoFile: Upload
description: String @goField(omittable: true)
websiteUrl: String @goField(omittable: true)
email: String @goField(omittable: true)
headquarterAddress: String @goField(omittable: true)
}
input DeleteOrganizationInput {
organizationId: ID!
}
input DeleteOrganizationHorizontalLogoInput {
organizationId: ID!
}
input SessionPolicyInput {
maxSessionDurationHours: Int
idleTimeoutMinutes: Int
@@ -2977,6 +3045,10 @@ type DeleteOrganizationPayload {
deletedOrganizationId: ID!
}
type DeleteOrganizationHorizontalLogoPayload {
organization: Organization!
}
type InviteMemberPayload {
invitationEdge: InvitationEdge!
}
@@ -3241,6 +3313,17 @@ func (ec *executionContext) field_Mutation_deleteInvitation_args(ctx context.Con
return args, nil
}
func (ec *executionContext) field_Mutation_deleteOrganizationHorizontalLogo_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error
args := map[string]any{}
arg0, err := graphql.ProcessArgField(ctx, rawArgs, "input", ec.unmarshalNDeleteOrganizationHorizontalLogoInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐDeleteOrganizationHorizontalLogoInput)
if err != nil {
return nil, err
}
args["input"] = arg0
return args, nil
}
func (ec *executionContext) field_Mutation_deleteOrganization_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error
args := map[string]any{}
@@ -3855,6 +3938,14 @@ func (ec *executionContext) fieldContext_CreateOrganizationPayload_organization(
return ec.fieldContext_Organization_logoUrl(ctx, field)
case "horizontalLogoUrl":
return ec.fieldContext_Organization_horizontalLogoUrl(ctx, field)
case "email":
return ec.fieldContext_Organization_email(ctx, field)
case "description":
return ec.fieldContext_Organization_description(ctx, field)
case "websiteUrl":
return ec.fieldContext_Organization_websiteUrl(ctx, field)
case "headquarterAddress":
return ec.fieldContext_Organization_headquarterAddress(ctx, field)
case "createdAt":
return ec.fieldContext_Organization_createdAt(ctx, field)
case "updatedAt":
@@ -4153,6 +4244,65 @@ func (ec *executionContext) fieldContext_DeleteInvitationPayload_deletedInvitati
return fc, nil
}
func (ec *executionContext) _DeleteOrganizationHorizontalLogoPayload_organization(ctx context.Context, field graphql.CollectedField, obj *types.DeleteOrganizationHorizontalLogoPayload) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
ec.fieldContext_DeleteOrganizationHorizontalLogoPayload_organization,
func(ctx context.Context) (any, error) {
return obj.Organization, nil
},
nil,
ec.marshalNOrganization2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐOrganization,
true,
true,
)
}
func (ec *executionContext) fieldContext_DeleteOrganizationHorizontalLogoPayload_organization(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "DeleteOrganizationHorizontalLogoPayload",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name {
case "id":
return ec.fieldContext_Organization_id(ctx, field)
case "name":
return ec.fieldContext_Organization_name(ctx, field)
case "logoUrl":
return ec.fieldContext_Organization_logoUrl(ctx, field)
case "horizontalLogoUrl":
return ec.fieldContext_Organization_horizontalLogoUrl(ctx, field)
case "email":
return ec.fieldContext_Organization_email(ctx, field)
case "description":
return ec.fieldContext_Organization_description(ctx, field)
case "websiteUrl":
return ec.fieldContext_Organization_websiteUrl(ctx, field)
case "headquarterAddress":
return ec.fieldContext_Organization_headquarterAddress(ctx, field)
case "createdAt":
return ec.fieldContext_Organization_createdAt(ctx, field)
case "updatedAt":
return ec.fieldContext_Organization_updatedAt(ctx, field)
case "members":
return ec.fieldContext_Organization_members(ctx, field)
case "invitations":
return ec.fieldContext_Organization_invitations(ctx, field)
case "samlConfigurations":
return ec.fieldContext_Organization_samlConfigurations(ctx, field)
case "availableApplications":
return ec.fieldContext_Organization_availableApplications(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type Organization", field.Name)
},
}
return fc, nil
}
func (ec *executionContext) _DeleteOrganizationPayload_deletedOrganizationId(ctx context.Context, field graphql.CollectedField, obj *types.DeleteOrganizationPayload) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
@@ -5257,6 +5407,14 @@ func (ec *executionContext) fieldContext_IdentityProfile_organization(_ context.
return ec.fieldContext_Organization_logoUrl(ctx, field)
case "horizontalLogoUrl":
return ec.fieldContext_Organization_horizontalLogoUrl(ctx, field)
case "email":
return ec.fieldContext_Organization_email(ctx, field)
case "description":
return ec.fieldContext_Organization_description(ctx, field)
case "websiteUrl":
return ec.fieldContext_Organization_websiteUrl(ctx, field)
case "headquarterAddress":
return ec.fieldContext_Organization_headquarterAddress(ctx, field)
case "createdAt":
return ec.fieldContext_Organization_createdAt(ctx, field)
case "updatedAt":
@@ -5569,6 +5727,14 @@ func (ec *executionContext) fieldContext_Invitation_organization(_ context.Conte
return ec.fieldContext_Organization_logoUrl(ctx, field)
case "horizontalLogoUrl":
return ec.fieldContext_Organization_horizontalLogoUrl(ctx, field)
case "email":
return ec.fieldContext_Organization_email(ctx, field)
case "description":
return ec.fieldContext_Organization_description(ctx, field)
case "websiteUrl":
return ec.fieldContext_Organization_websiteUrl(ctx, field)
case "headquarterAddress":
return ec.fieldContext_Organization_headquarterAddress(ctx, field)
case "createdAt":
return ec.fieldContext_Organization_createdAt(ctx, field)
case "updatedAt":
@@ -6039,6 +6205,14 @@ func (ec *executionContext) fieldContext_Membership_organization(_ context.Conte
return ec.fieldContext_Organization_logoUrl(ctx, field)
case "horizontalLogoUrl":
return ec.fieldContext_Organization_horizontalLogoUrl(ctx, field)
case "email":
return ec.fieldContext_Organization_email(ctx, field)
case "description":
return ec.fieldContext_Organization_description(ctx, field)
case "websiteUrl":
return ec.fieldContext_Organization_websiteUrl(ctx, field)
case "headquarterAddress":
return ec.fieldContext_Organization_headquarterAddress(ctx, field)
case "createdAt":
return ec.fieldContext_Organization_createdAt(ctx, field)
case "updatedAt":
@@ -7643,6 +7817,69 @@ func (ec *executionContext) fieldContext_Mutation_deleteOrganization(ctx context
return fc, nil
}
func (ec *executionContext) _Mutation_deleteOrganizationHorizontalLogo(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
ec.fieldContext_Mutation_deleteOrganizationHorizontalLogo,
func(ctx context.Context) (any, error) {
fc := graphql.GetFieldContext(ctx)
return ec.resolvers.Mutation().DeleteOrganizationHorizontalLogo(ctx, fc.Args["input"].(types.DeleteOrganizationHorizontalLogoInput))
},
func(ctx context.Context, next graphql.Resolver) graphql.Resolver {
directive0 := next
directive1 := func(ctx context.Context) (any, error) {
required, err := ec.unmarshalNSessionRequirement2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐSessionRequirement(ctx, "PRESENT")
if err != nil {
var zeroVal *types.DeleteOrganizationHorizontalLogoPayload
return zeroVal, err
}
if ec.directives.Session == nil {
var zeroVal *types.DeleteOrganizationHorizontalLogoPayload
return zeroVal, errors.New("directive session is not implemented")
}
return ec.directives.Session(ctx, nil, directive0, required)
}
next = directive1
return next
},
ec.marshalODeleteOrganizationHorizontalLogoPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐDeleteOrganizationHorizontalLogoPayload,
true,
false,
)
}
func (ec *executionContext) fieldContext_Mutation_deleteOrganizationHorizontalLogo(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Mutation",
Field: field,
IsMethod: true,
IsResolver: true,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name {
case "organization":
return ec.fieldContext_DeleteOrganizationHorizontalLogoPayload_organization(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type DeleteOrganizationHorizontalLogoPayload", field.Name)
},
}
defer func() {
if r := recover(); r != nil {
err = ec.Recover(ctx, r)
ec.Error(ctx, err)
}
}()
ctx = graphql.WithFieldContext(ctx, fc)
if fc.Args, err = ec.field_Mutation_deleteOrganizationHorizontalLogo_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
ec.Error(ctx, err)
return fc, err
}
return fc, nil
}
func (ec *executionContext) _Mutation_inviteMember(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
@@ -8200,6 +8437,122 @@ func (ec *executionContext) fieldContext_Organization_horizontalLogoUrl(_ contex
return fc, nil
}
func (ec *executionContext) _Organization_email(ctx context.Context, field graphql.CollectedField, obj *types.Organization) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
ec.fieldContext_Organization_email,
func(ctx context.Context) (any, error) {
return obj.Email, nil
},
nil,
ec.marshalOString2ᚖstring,
true,
false,
)
}
func (ec *executionContext) fieldContext_Organization_email(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Organization",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type String does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _Organization_description(ctx context.Context, field graphql.CollectedField, obj *types.Organization) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
ec.fieldContext_Organization_description,
func(ctx context.Context) (any, error) {
return obj.Description, nil
},
nil,
ec.marshalOString2ᚖstring,
true,
false,
)
}
func (ec *executionContext) fieldContext_Organization_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Organization",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type String does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _Organization_websiteUrl(ctx context.Context, field graphql.CollectedField, obj *types.Organization) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
ec.fieldContext_Organization_websiteUrl,
func(ctx context.Context) (any, error) {
return obj.WebsiteURL, nil
},
nil,
ec.marshalOString2ᚖstring,
true,
false,
)
}
func (ec *executionContext) fieldContext_Organization_websiteUrl(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Organization",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type String does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _Organization_headquarterAddress(ctx context.Context, field graphql.CollectedField, obj *types.Organization) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
ec.fieldContext_Organization_headquarterAddress,
func(ctx context.Context) (any, error) {
return obj.HeadquarterAddress, nil
},
nil,
ec.marshalOString2ᚖstring,
true,
false,
)
}
func (ec *executionContext) fieldContext_Organization_headquarterAddress(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Organization",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type String does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _Organization_createdAt(ctx context.Context, field graphql.CollectedField, obj *types.Organization) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
@@ -8847,6 +9200,14 @@ func (ec *executionContext) fieldContext_Permission_organization(_ context.Conte
return ec.fieldContext_Organization_logoUrl(ctx, field)
case "horizontalLogoUrl":
return ec.fieldContext_Organization_horizontalLogoUrl(ctx, field)
case "email":
return ec.fieldContext_Organization_email(ctx, field)
case "description":
return ec.fieldContext_Organization_description(ctx, field)
case "websiteUrl":
return ec.fieldContext_Organization_websiteUrl(ctx, field)
case "headquarterAddress":
return ec.fieldContext_Organization_headquarterAddress(ctx, field)
case "createdAt":
return ec.fieldContext_Organization_createdAt(ctx, field)
case "updatedAt":
@@ -9198,6 +9559,14 @@ func (ec *executionContext) fieldContext_PersonalAPIKey_organizations(_ context.
return ec.fieldContext_Organization_logoUrl(ctx, field)
case "horizontalLogoUrl":
return ec.fieldContext_Organization_horizontalLogoUrl(ctx, field)
case "email":
return ec.fieldContext_Organization_email(ctx, field)
case "description":
return ec.fieldContext_Organization_description(ctx, field)
case "websiteUrl":
return ec.fieldContext_Organization_websiteUrl(ctx, field)
case "headquarterAddress":
return ec.fieldContext_Organization_headquarterAddress(ctx, field)
case "createdAt":
return ec.fieldContext_Organization_createdAt(ctx, field)
case "updatedAt":
@@ -11569,6 +11938,14 @@ func (ec *executionContext) fieldContext_UpdateOrganizationPayload_organization(
return ec.fieldContext_Organization_logoUrl(ctx, field)
case "horizontalLogoUrl":
return ec.fieldContext_Organization_horizontalLogoUrl(ctx, field)
case "email":
return ec.fieldContext_Organization_email(ctx, field)
case "description":
return ec.fieldContext_Organization_description(ctx, field)
case "websiteUrl":
return ec.fieldContext_Organization_websiteUrl(ctx, field)
case "headquarterAddress":
return ec.fieldContext_Organization_headquarterAddress(ctx, field)
case "createdAt":
return ec.fieldContext_Organization_createdAt(ctx, field)
case "updatedAt":
@@ -13578,6 +13955,33 @@ func (ec *executionContext) unmarshalInputDeleteInvitationInput(ctx context.Cont
return it, nil
}
func (ec *executionContext) unmarshalInputDeleteOrganizationHorizontalLogoInput(ctx context.Context, obj any) (types.DeleteOrganizationHorizontalLogoInput, error) {
var it types.DeleteOrganizationHorizontalLogoInput
asMap := map[string]any{}
for k, v := range obj.(map[string]any) {
asMap[k] = v
}
fieldsInOrder := [...]string{"organizationId"}
for _, k := range fieldsInOrder {
v, ok := asMap[k]
if !ok {
continue
}
switch k {
case "organizationId":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("organizationId"))
data, err := ec.unmarshalNID2goᚗproboᚗincᚋproboᚋpkgᚋgidᚐGID(ctx, v)
if err != nil {
return it, err
}
it.OrganizationID = data
}
}
return it, nil
}
func (ec *executionContext) unmarshalInputDeleteOrganizationInput(ctx context.Context, obj any) (types.DeleteOrganizationInput, error) {
var it types.DeleteOrganizationInput
asMap := map[string]any{}
@@ -14253,7 +14657,7 @@ func (ec *executionContext) unmarshalInputUpdateOrganizationInput(ctx context.Co
asMap[k] = v
}
fieldsInOrder := [...]string{"organizationId", "name", "logoFile", "horizontalLogoFile"}
fieldsInOrder := [...]string{"organizationId", "name", "logoFile", "horizontalLogoFile", "description", "websiteUrl", "email", "headquarterAddress"}
for _, k := range fieldsInOrder {
v, ok := asMap[k]
if !ok {
@@ -14280,14 +14684,42 @@ func (ec *executionContext) unmarshalInputUpdateOrganizationInput(ctx context.Co
if err != nil {
return it, err
}
it.LogoFile = graphql.OmittableOf(data)
it.LogoFile = data
case "horizontalLogoFile":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("horizontalLogoFile"))
data, err := ec.unmarshalOUpload2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚐUpload(ctx, v)
if err != nil {
return it, err
}
it.HorizontalLogoFile = graphql.OmittableOf(data)
it.HorizontalLogoFile = data
case "description":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("description"))
data, err := ec.unmarshalOString2ᚖstring(ctx, v)
if err != nil {
return it, err
}
it.Description = graphql.OmittableOf(data)
case "websiteUrl":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("websiteUrl"))
data, err := ec.unmarshalOString2ᚖstring(ctx, v)
if err != nil {
return it, err
}
it.WebsiteURL = graphql.OmittableOf(data)
case "email":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("email"))
data, err := ec.unmarshalOString2ᚖstring(ctx, v)
if err != nil {
return it, err
}
it.Email = graphql.OmittableOf(data)
case "headquarterAddress":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("headquarterAddress"))
data, err := ec.unmarshalOString2ᚖstring(ctx, v)
if err != nil {
return it, err
}
it.HeadquarterAddress = graphql.OmittableOf(data)
}
}
@@ -15043,6 +15475,45 @@ func (ec *executionContext) _DeleteInvitationPayload(ctx context.Context, sel as
return out
}
var deleteOrganizationHorizontalLogoPayloadImplementors = []string{"DeleteOrganizationHorizontalLogoPayload"}
func (ec *executionContext) _DeleteOrganizationHorizontalLogoPayload(ctx context.Context, sel ast.SelectionSet, obj *types.DeleteOrganizationHorizontalLogoPayload) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, deleteOrganizationHorizontalLogoPayloadImplementors)
out := graphql.NewFieldSet(fields)
deferred := make(map[string]*graphql.FieldSet)
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("DeleteOrganizationHorizontalLogoPayload")
case "organization":
out.Values[i] = ec._DeleteOrganizationHorizontalLogoPayload_organization(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}
}
out.Dispatch(ctx)
if out.Invalids > 0 {
return graphql.Null
}
atomic.AddInt32(&ec.deferred, int32(len(deferred)))
for label, dfs := range deferred {
ec.processDeferredGroup(graphql.DeferredGroup{
Label: label,
Path: graphql.GetPath(ctx),
FieldSet: dfs,
Context: ctx,
})
}
return out
}
var deleteOrganizationPayloadImplementors = []string{"DeleteOrganizationPayload"}
func (ec *executionContext) _DeleteOrganizationPayload(ctx context.Context, sel ast.SelectionSet, obj *types.DeleteOrganizationPayload) graphql.Marshaler {
@@ -16100,6 +16571,10 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
return ec._Mutation_deleteOrganization(ctx, field)
})
case "deleteOrganizationHorizontalLogo":
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
return ec._Mutation_deleteOrganizationHorizontalLogo(ctx, field)
})
case "inviteMember":
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
return ec._Mutation_inviteMember(ctx, field)
@@ -16238,6 +16713,14 @@ func (ec *executionContext) _Organization(ctx context.Context, sel ast.Selection
}
out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
case "email":
out.Values[i] = ec._Organization_email(ctx, field, obj)
case "description":
out.Values[i] = ec._Organization_description(ctx, field, obj)
case "websiteUrl":
out.Values[i] = ec._Organization_websiteUrl(ctx, field, obj)
case "headquarterAddress":
out.Values[i] = ec._Organization_headquarterAddress(ctx, field, obj)
case "createdAt":
out.Values[i] = ec._Organization_createdAt(ctx, field, obj)
if out.Values[i] == graphql.Null {
@@ -18669,6 +19152,11 @@ func (ec *executionContext) unmarshalNDeleteInvitationInput2goᚗproboᚗincᚋp
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) unmarshalNDeleteOrganizationHorizontalLogoInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐDeleteOrganizationHorizontalLogoInput(ctx context.Context, v any) (types.DeleteOrganizationHorizontalLogoInput, error) {
res, err := ec.unmarshalInputDeleteOrganizationHorizontalLogoInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) unmarshalNDeleteOrganizationInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐDeleteOrganizationInput(ctx context.Context, v any) (types.DeleteOrganizationInput, error) {
res, err := ec.unmarshalInputDeleteOrganizationInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err)
@@ -20085,6 +20573,13 @@ func (ec *executionContext) marshalODeleteInvitationPayload2ᚖgoᚗproboᚗinc
return ec._DeleteInvitationPayload(ctx, sel, v)
}
func (ec *executionContext) marshalODeleteOrganizationHorizontalLogoPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐDeleteOrganizationHorizontalLogoPayload(ctx context.Context, sel ast.SelectionSet, v *types.DeleteOrganizationHorizontalLogoPayload) graphql.Marshaler {
if v == nil {
return graphql.Null
}
return ec._DeleteOrganizationHorizontalLogoPayload(ctx, sel, v)
}
func (ec *executionContext) marshalODeleteOrganizationPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐDeleteOrganizationPayload(ctx context.Context, sel ast.SelectionSet, v *types.DeleteOrganizationPayload) graphql.Marshaler {
if v == nil {
return graphql.Null

View File

@@ -24,9 +24,13 @@ type (
func NewOrganization(organization *coredata.Organization) *Organization {
return &Organization{
ID: organization.ID,
Name: organization.Name,
CreatedAt: organization.CreatedAt,
UpdatedAt: organization.UpdatedAt,
ID: organization.ID,
Name: organization.Name,
Email: organization.Email,
Description: organization.Description,
WebsiteURL: organization.WebsiteURL,
HeadquarterAddress: organization.HeadquarterAddress,
CreatedAt: organization.CreatedAt,
UpdatedAt: organization.UpdatedAt,
}
}

View File

@@ -139,6 +139,14 @@ type DeleteInvitationPayload struct {
DeletedInvitationID gid.GID `json:"deletedInvitationId"`
}
type DeleteOrganizationHorizontalLogoInput struct {
OrganizationID gid.GID `json:"organizationId"`
}
type DeleteOrganizationHorizontalLogoPayload struct {
Organization *Organization `json:"organization"`
}
type DeleteOrganizationInput struct {
OrganizationID gid.GID `json:"organizationId"`
}
@@ -264,6 +272,10 @@ type Organization struct {
Name string `json:"name"`
LogoURL *string `json:"logoUrl,omitempty"`
HorizontalLogoURL *string `json:"horizontalLogoUrl,omitempty"`
Email *string `json:"email,omitempty"`
Description *string `json:"description,omitempty"`
WebsiteURL *string `json:"websiteUrl,omitempty"`
HeadquarterAddress *string `json:"headquarterAddress,omitempty"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
Members *MembershipConnection `json:"members,omitempty"`
@@ -516,10 +528,14 @@ type UpdateIdentityProfilePayload struct {
}
type UpdateOrganizationInput struct {
OrganizationID gid.GID `json:"organizationId"`
Name *string `json:"name,omitempty"`
LogoFile graphql.Omittable[*graphql.Upload] `json:"logoFile,omitempty"`
HorizontalLogoFile graphql.Omittable[*graphql.Upload] `json:"horizontalLogoFile,omitempty"`
OrganizationID gid.GID `json:"organizationId"`
Name *string `json:"name,omitempty"`
LogoFile *graphql.Upload `json:"logoFile,omitempty"`
HorizontalLogoFile *graphql.Upload `json:"horizontalLogoFile,omitempty"`
Description graphql.Omittable[*string] `json:"description,omitempty"`
WebsiteURL graphql.Omittable[*string] `json:"websiteUrl,omitempty"`
Email graphql.Omittable[*string] `json:"email,omitempty"`
HeadquarterAddress graphql.Omittable[*string] `json:"headquarterAddress,omitempty"`
}
type UpdateOrganizationPayload struct {

View File

@@ -637,12 +637,36 @@ func (r *mutationResolver) CreateOrganization(ctx context.Context, input types.C
// UpdateOrganization is the resolver for the updateOrganization field.
func (r *mutationResolver) UpdateOrganization(ctx context.Context, input types.UpdateOrganizationInput) (*types.UpdateOrganizationPayload, error) {
req := &iam.UpdateOrganizationRequest{
Name: input.Name,
Description: UnwrapOmittable(input.Description),
WebsiteURL: UnwrapOmittable(input.WebsiteURL),
Email: UnwrapOmittable(input.Email),
HeadquarterAddress: UnwrapOmittable(input.HeadquarterAddress),
}
if input.LogoFile != nil {
req.LogoFile = &iam.UploadedFile{
Filename: input.LogoFile.Filename,
ContentType: input.LogoFile.ContentType,
Size: input.LogoFile.Size,
Content: input.LogoFile.File,
}
}
if input.HorizontalLogoFile != nil {
req.HorizontalLogoFile = &iam.UploadedFile{
Filename: input.HorizontalLogoFile.Filename,
ContentType: input.HorizontalLogoFile.ContentType,
Size: input.HorizontalLogoFile.Size,
Content: input.HorizontalLogoFile.File,
}
}
organization, err := r.iam.OrganizationService.UpdateOrganization(
ctx,
input.OrganizationID,
&iam.UpdateOrganizationRequest{
Name: input.Name,
},
req,
)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot update organization", log.Error(err))
@@ -670,6 +694,11 @@ func (r *mutationResolver) DeleteOrganization(ctx context.Context, input types.D
return &types.DeleteOrganizationPayload{DeletedOrganizationID: input.OrganizationID}, nil
}
// DeleteOrganizationHorizontalLogo is the resolver for the deleteOrganizationHorizontalLogo field.
func (r *mutationResolver) DeleteOrganizationHorizontalLogo(ctx context.Context, input types.DeleteOrganizationHorizontalLogoInput) (*types.DeleteOrganizationHorizontalLogoPayload, error) {
panic(fmt.Errorf("not implemented: DeleteOrganizationHorizontalLogo - deleteOrganizationHorizontalLogo"))
}
// InviteMember is the resolver for the inviteMember field.
func (r *mutationResolver) InviteMember(ctx context.Context, input types.InviteMemberInput) (*types.InviteMemberPayload, error) {
invitation, err := r.iam.OrganizationService.InviteMember(