-
Notifications
You must be signed in to change notification settings - Fork 1.5k
refactor(site): migrate organization settings form off MUI #27718
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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"> | ||
| <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} | ||
| /> | ||
| </> | ||
| ); | ||
| }; | ||
| 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, | ||
|
|
@@ -12,6 +11,10 @@ const meta: Meta<typeof OrganizationSettingsPageView> = { | |
| component: OrganizationSettingsPageView, | ||
| args: { | ||
| organization: MockOrganization, | ||
| onSubmit: action("onSubmit"), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When this commit replaces the organization info inputs, the stories register AGENTS.md reference: site/AGENTS.md:L9-L10 Useful? React with 👍 / 👎. |
||
| onDeleteOrganization: action("onDeleteOrganization"), | ||
| shareableWorkspaceOwners: "everyone", | ||
| onChangeShareableOwners: action("onChangeShareableOwners"), | ||
| }, | ||
| }; | ||
|
|
||
|
|
@@ -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); | ||
| }, | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.