GitFOSS
fe04f67 (parent 5c6496d)5/9/2026, 8:25:56 AM
.ts
TypeScript
(application/typescript)
// 1st-party
import type { ReactIsland } from "@ethicdevs/react-monolith";
// 3rd-party
import React, { useCallback, useEffect, useState } from "react";
// generated via script[generate:prisma]
import type { Organization, ResourceVisibility } from "@prisma/client";
// app
import type { WithThemeSchemeProp } from "../types";
import { slugify } from "../utils/shared";
import { Button } from "../components/Button.styled";
import { Card } from "../components/Card.styled";
import { Grid } from "../components/Grid";
import { Select, TextInput } from "../components/TextInput.styled";

export interface RepositoryForkFormProps {
  disabled?: boolean;
  availableParentOrgs: Organization[];
  editMode?: boolean;
  initialValues?: {
    target_org_slug: string;
    target_repo_display_name: string;
    target_repo_slug: string;
    target_repo_visibility: ResourceVisibility;
  };
}

const RepositoryForkForm: ReactIsland<
  RepositoryForkFormProps & WithThemeSchemeProp
> = ({
  availableParentOrgs,
  themeScheme,
  disabled = false,
  editMode = false,
  initialValues = undefined,
}) => {
  const [displayName, setDisplayName] = useState<string>(
    initialValues?.target_repo_display_name || "",
  );
  const [slug, setSlug] = useState<string>(
    initialValues?.target_repo_slug ||
      slugify(initialValues?.target_repo_display_name || ""),
  );
  const [slugInputDirty, setSlugInputDirty] = useState<boolean>(false);

  editMode;

  const onDisplayNameInputChange = useCallback(
    (ev: React.ChangeEvent<HTMLInputElement>) => {
      if (ev == null || ev.target == null || ev.target.value == null) {
        return undefined;
      }
      setDisplayName(ev.target.value);
      return undefined;
    },
    [setDisplayName],
  );

  const onSlugInputChange = useCallback(
    (ev: React.ChangeEvent<HTMLInputElement>) => {
      if (ev == null || ev.target == null || ev.target.value == null) {
        return undefined;
      }
      setSlug(ev.target.value);
      // only once, avoid useless re-renders for nothing
      if (slugInputDirty === false) {
        setSlugInputDirty(true);
      }
      return undefined;
    },
    [slugInputDirty, setSlug, setSlugInputDirty],
  );

  // An effect that will update the slug if it isn't dirty when the display name
  // input value (displayName) has changed.
  useEffect(() => {
    const nextSlug = slugify(displayName);
    if (
      slugInputDirty === false &&
      displayName != null &&
      displayName.trim() !== "" &&
      nextSlug !== slug
    ) {
      setSlug(nextSlug);
    }
  }, [displayName, slug, slugInputDirty, setSlug]);

  return (
    <Grid.Col fluid nowrap gap={24}>
      <Grid.Col fluid nowrap gap={8}>
        <label>
          <strong>Fork details</strong>
        </label>
        <Card themeScheme={themeScheme}>
          <Grid.Col fluid nowrap gap={16}>
            {/* Repository Name */}
            <Grid.Col fluid nowrap gap={4}>
              <label htmlFor={"target_repo_display_name"}>
                Repository Name<span style={{ color: "red" }}>*</span>:
              </label>
              <TextInput
                themeScheme={themeScheme}
                name={"target_repo_display_name"}
                disabled={disabled}
                onChange={onDisplayNameInputChange}
                placeholder={"i.e. My Super Project"}
                required
                style={styles.inputMaxWidth}
                type={"text"}
                value={displayName}
              />
            </Grid.Col>
            <Grid.Row fluid nowrap alignItems={"center"} gap={16}>
              {/* Parent Organization Select */}
              <Grid.Col fluid nowrap gap={4}>
                <label htmlFor={"target_org_slug"}>
                  Owner Organization<span style={{ color: "red" }}>*</span>:
                </label>
                <Select
                  themeScheme={themeScheme}
                  disabled={disabled}
                  defaultValue={
                    availableParentOrgs.length >= 1
                      ? availableParentOrgs[0].slug
                      : initialValues?.target_org_slug
                  }
                  name={"target_org_slug"}
                  required
                  style={styles.inputMaxWidth}
                >
                  {availableParentOrgs.map((org) => (
                    <option key={org.id} value={org.slug}>
                      {org.displayName || org.slug}
                    </option>
                  ))}
                </Select>
              </Grid.Col>
              {/* Repository Slug */}
              <Grid.Col fluid nowrap gap={4}>
                <label htmlFor={"target_repo_slug"}>
                  Repository Slug<span style={{ color: "red" }}>*</span>:
                </label>
                <TextInput
                  themeScheme={themeScheme}
                  name={"target_repo_slug"}
                  disabled={disabled}
                  onChange={onSlugInputChange}
                  placeholder={"i.e. my-super-project"}
                  required
                  style={styles.inputMaxWidth}
                  type={"text"}
                  value={slug}
                />
              </Grid.Col>
            </Grid.Row>
            {/* Repository Visibility */}
            <Grid.Col fluid nowrap gap={4}>
              <label htmlFor={"target_repo_visibility"}>
                Repository Visibility<span style={{ color: "red" }}>*</span>:
              </label>
              <Select
                themeScheme={themeScheme}
                disabled={disabled}
                defaultValue={
                  initialValues?.target_repo_visibility || "PRIVATE"
                }
                name={"target_repo_visibility"}
                style={styles.inputMaxWidth}
              >
                <option key={"private"} value={"PRIVATE"}>
                  Private
                </option>
                <option key={"unlisted"} value={"UNLISTED"}>
                  Unlisted
                </option>
                <option key={"public"} value={"PUBLIC"}>
                  Public
                </option>
              </Select>
            </Grid.Col>
          </Grid.Col>
        </Card>
      </Grid.Col>

      {/* Submit Button */}
      <Button style={styles.inputMaxWidth} type={"submit"} disabled={disabled}>
        Fork Repository!
      </Button>
    </Grid.Col>
  );
};

const styles = {
  alignSelfEnd: {
    alignSelf: "flex-end",
  },
  inputMaxWidth: {
    minWidth: "100%",
    width: "100%",
    maxWidth: "100%",
  },
  labelFlexOne: {
    flex: 1,
    marginRight: 8,
  },
  shortDescriptionTextArea: {
    minHeight: 75,
    height: 75,
    maxHeight: 75,
  },
};

RepositoryForkForm.displayName = "RepositoryForkForm";
export default RepositoryForkForm;