Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { type FC, useState } from "react";
import { Button } from "#/components/Button/Button";
import { DeleteDialog } from "#/components/Dialog/DeleteDialog/DeleteDialog";
import { FormSection, HorizontalForm } from "#/components/Form/Form";

type DeleteOrganizationSectionProps = {
organizationName: string;
onDeleteOrganization: () => void;
};

export const DeleteOrganizationSection: FC<DeleteOrganizationSectionProps> = ({
organizationName,
onDeleteOrganization,
}) => {
const [isDeleting, setIsDeleting] = useState(false);

return (
<>
<HorizontalForm className="mt-12">
Comment thread
jakehwll marked this conversation as resolved.
<FormSection
title="Delete Organization"
description="Delete your organization permanently."
>
<div className="flex flex-col gap-4 flex-grow">
<div className="flex bg-surface-red items-center justify-between border border-solid border-border-destructive rounded-md p-3 pl-4 gap-2">
<span>Deleting an organization is irreversible.</span>
<Button
variant="destructive"
onClick={() => setIsDeleting(true)}
className="min-w-fit"
>
Delete this organization
</Button>
</div>
</div>
</FormSection>
</HorizontalForm>

<DeleteDialog
isOpen={isDeleting}
onConfirm={async () => {
await onDeleteOrganization();
setIsDeleting(false);
}}
onCancel={() => setIsDeleting(false)}
entity="organization"
name={organizationName}
/>
</>
);
};
160 changes: 160 additions & 0 deletions site/src/pages/OrganizationSettingsPage/OrganizationInfoForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { useFormik } from "formik";
import type { FC } from "react";
import * as Yup from "yup";
import { isApiValidationError } from "#/api/errors";
import type {
Organization,
UpdateOrganizationRequest,
} from "#/api/typesGenerated";
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
import { Button } from "#/components/Button/Button";
import {
FormFields,
FormFooter,
FormSection,
VerticalForm,
} from "#/components/Form/Form";
import { FormField } from "#/components/FormField/FormField";
import { IconField } from "#/components/IconField/IconField";
import { Label } from "#/components/Label/Label";
import { Spinner } from "#/components/Spinner/Spinner";
import { Textarea } from "#/components/Textarea/Textarea";
import { cn } from "#/utils/cn";
import {
displayNameValidator,
getFormHelpers,
nameValidator,
onChangeTrimmed,
} from "#/utils/formUtils";

const MAX_DESCRIPTION_CHAR_LIMIT = 128;
const MAX_DESCRIPTION_MESSAGE = `Please enter a description that is no longer than ${MAX_DESCRIPTION_CHAR_LIMIT} characters.`;

const validationSchema = Yup.object({
name: nameValidator("Name"),
display_name: displayNameValidator("Display name"),
description: Yup.string().max(
MAX_DESCRIPTION_CHAR_LIMIT,
MAX_DESCRIPTION_MESSAGE,
),
});

type OrganizationInfoFormProps = {
organization: Organization;
error: unknown;
onSubmit: (values: UpdateOrganizationRequest) => Promise<void>;
};

export const OrganizationInfoForm: FC<OrganizationInfoFormProps> = ({
organization,
error,
onSubmit,
}) => {
const form = useFormik<UpdateOrganizationRequest>({
initialValues: {
name: organization.name,
display_name: organization.display_name,
description: organization.description,
icon: organization.icon,
},
validationSchema,
onSubmit,
enableReinitialize: true,
});
const getFieldHelpers = getFormHelpers(form, error);
const descriptionField = getFieldHelpers("description", {
maxLength: MAX_DESCRIPTION_CHAR_LIMIT,
});
const descriptionErrorId = `${descriptionField.id}-error`;
const descriptionHelperId = `${descriptionField.id}-helper`;

return (
<>
{Boolean(error) && !isApiValidationError(error) && (
<div className="mb-8">
<ErrorAlert error={error} />
</div>
)}

<VerticalForm
onSubmit={form.handleSubmit}
aria-label="Organization settings form"
>
<FormSection
title="Info"
description="The name and description of the organization."
>
<fieldset
disabled={form.isSubmitting}
className="border-0 p-0 m-0 w-full"
>
<FormFields>
<FormField
field={getFieldHelpers("name")}
label="Slug"
onChange={onChangeTrimmed(form)}
autoFocus
/>
<FormField
field={getFieldHelpers("display_name")}
label="Display name"
/>
<div className="flex flex-col gap-2">
<Label htmlFor={descriptionField.id}>Description</Label>
<Textarea
id={descriptionField.id}
name={descriptionField.name}
value={descriptionField.value}
onChange={descriptionField.onChange}
onBlur={descriptionField.onBlur}
rows={2}
aria-invalid={descriptionField.error}
aria-describedby={
descriptionField.error
? descriptionErrorId
: descriptionField.helperText
? descriptionHelperId
: undefined
}
className={cn(
descriptionField.error && "border-border-destructive",
)}
/>
{descriptionField.error ? (
<span
id={descriptionErrorId}
className="text-xs text-content-destructive"
>
{descriptionField.helperText}
</span>
) : (
descriptionField.helperText && (
<span
id={descriptionHelperId}
className="text-xs text-content-secondary"
>
{descriptionField.helperText}
</span>
)
)}
</div>
<IconField
{...getFieldHelpers("icon")}
onChange={onChangeTrimmed(form)}
fullWidth
onPickEmoji={(value) => form.setFieldValue("icon", value)}
/>
</FormFields>
</fieldset>
</FormSection>

<FormFooter>
<Button type="submit" disabled={form.isSubmitting}>
<Spinner loading={form.isSubmitting} />
Save
</Button>
</FormFooter>
</VerticalForm>
</>
);
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { action } from "storybook/actions";
import { userEvent, within } from "storybook/test";
import {
MockDefaultOrganization,
MockOrganization,
Expand All @@ -12,6 +11,10 @@ const meta: Meta<typeof OrganizationSettingsPageView> = {
component: OrganizationSettingsPageView,
args: {
organization: MockOrganization,
onSubmit: action("onSubmit"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add interaction coverage for the migrated form

When this commit replaces the organization info inputs, the stories register onSubmit but never use a play function to edit, validate, or submit the new controls, so regressions in trimming, validation, and submission will pass the Storybook suite. Add a story interaction that exercises these behaviors and asserts the submitted values.

AGENTS.md reference: site/AGENTS.md:L9-L10

Useful? React with 👍 / 👎.

onDeleteOrganization: action("onDeleteOrganization"),
shareableWorkspaceOwners: "everyone",
onChangeShareableOwners: action("onChangeShareableOwners"),
},
};

Expand All @@ -25,62 +28,3 @@ export const DefaultOrg: Story = {
organization: MockDefaultOrganization,
},
};

export const SharingDisabled: Story = {
args: {
shareableWorkspaceOwners: "none",
onChangeShareableOwners: action("onChangeShareableOwners"),
},
};

export const SharingServiceAccountsOnly: Story = {
args: {
shareableWorkspaceOwners: "service_accounts",
onChangeShareableOwners: action("onChangeShareableOwners"),
},
};

export const SharingEveryone: Story = {
args: {
shareableWorkspaceOwners: "everyone",
onChangeShareableOwners: action("onChangeShareableOwners"),
},
};

export const SharingGloballyDisabled: Story = {
args: {
shareableWorkspaceOwners: "none",
workspaceSharingGloballyDisabled: true,
onChangeShareableOwners: action("onChangeShareableOwners"),
},
};

export const DisableSharingDialog: Story = {
args: {
shareableWorkspaceOwners: "everyone",
onChangeShareableOwners: action("onChangeShareableOwners"),
},
play: async ({ canvasElement }) => {
const user = userEvent.setup();
const body = within(canvasElement.ownerDocument.body);
const checkbox = await body.findByRole("checkbox", {
name: /allow workspace sharing/i,
});
await user.click(checkbox);
},
};

export const RestrictToServiceAccountsDialog: Story = {
args: {
shareableWorkspaceOwners: "everyone",
onChangeShareableOwners: action("onChangeShareableOwners"),
},
play: async ({ canvasElement }) => {
const user = userEvent.setup();
const body = within(canvasElement.ownerDocument.body);
const radio = await body.findByRole("radio", {
name: /only service accounts/i,
});
await user.click(radio);
},
};
Loading
Loading