Hypercode/alex/hypercodePublic

Code

  1. hypercode
  2. views
  3. pages
  4. repository_settings.go
repository_settings.go439 lines
package pages

import (
	"fmt"
	"net/http"

	"github.com/hypercodehq/hypercode/database/models"
	"github.com/hypercodehq/libhtml"
	"github.com/hypercodehq/libhtml/attr"
	"github.com/hypercodehq/hypercode/views/components/layouts"
	"github.com/hypercodehq/hypercode/views/components/ui"
)

type CollaboratorData struct {
	Contributor *models.Contributor
	Username    string
}

type RepositorySettingsData struct {
	User                *models.User
	Repository          *models.Repository
	OwnerUsername       string
	Name                string
	DefaultBranch       string
	Visibility          string
	NameError           string
	DefaultBranchError  string
	VisibilityError     string
	GeneralSuccess      string
	DangerZoneSuccess   string
	StarCount           int64
	HasStarred          bool
	Collaborators       []CollaboratorData
	CollaboratorError   string
	CollaboratorSuccess string
	NewCollaborator     string
}

func RepositorySettings(r *http.Request, data *RepositorySettingsData) html.Node {
	if data == nil {
		data = &RepositorySettingsData{}
	}

	// Populate form values from repository data if not set
	if data.Repository != nil {
		if data.Name == "" {
			data.Name = data.Repository.Name
		}
		if data.DefaultBranch == "" {
			data.DefaultBranch = data.Repository.DefaultBranch
		}
		if data.Visibility == "" {
			data.Visibility = data.Repository.Visibility
		}
	}

	cloneURL := "https://" + r.Host + "/" + data.OwnerUsername + "/" + data.Repository.Name
	repositoryURL := cloneURL

	return layouts.Repository(r,
		"Settings - "+data.OwnerUsername+"/"+data.Repository.Name,
		layouts.RepositoryLayoutOptions{
			OwnerUsername: data.OwnerUsername,
			RepoName:      data.Repository.Name,
			CurrentTab:    "settings",
			IsPublic:      data.Repository.Visibility == "public",
			ShowSettings:  true,
			StarCount:     data.StarCount,
			HasStarred:    data.HasStarred,
			DefaultBranch: data.Repository.DefaultBranch,
			CloneURL:      cloneURL,
			RepositoryURL: repositoryURL,
		},
		html.Main(
			attr.Class("w-full mx-auto max-w-7xl space-y-6 py-8 px-4"),
			html.H1(
				attr.Class("font-semibold text-2xl mb-6"),
				html.Text("Repository Settings"),
			),

			// General Settings Card
			ui.Card(ui.CardProps{
				Title:       "General",
				Description: "Update repository information",
				Content: html.Div(
					attr.Class("space-y-4"),
					html.If(data.GeneralSuccess != "", html.Div(
						attr.Class("p-3 rounded-lg bg-emerald-50 dark:bg-emerald-900/20 border border-emerald-200 dark:border-emerald-800 text-emerald-800 dark:text-emerald-200 text-sm"),
						html.Text(data.GeneralSuccess),
					)),
					html.Form(
						attr.Id("general-settings-form"),
						attr.Method("POST"),
						attr.Action("/"+data.OwnerUsername+"/"+data.Repository.Name+"/settings/general"),
						attr.Class("space-y-4"),
						attr.Attribute{Key: "data-original-name", Value: data.Repository.Name},
						ui.FormField(ui.FormFieldProps{
							Label:       "Repository Name",
							Id:          "name",
							Name:        "name",
							Type:        "text",
							Placeholder: "my-awesome-project",
							Icon:        ui.IconRepository,
							Required:    true,
							Value:       data.Name,
							Error:       data.NameError,
						}),
						ui.FormField(ui.FormFieldProps{
							Label:       "Default Branch",
							Id:          "default_branch",
							Name:        "default_branch",
							Type:        "text",
							Placeholder: "main",
							Icon:        ui.IconGitBranch,
							Value:       data.DefaultBranch,
							Error:       data.DefaultBranchError,
						}),
						html.Div(
							attr.Class("space-y-2"),
							html.Label(
								attr.Class("label"),
								html.Text("Visibility"),
							),
							html.Div(
								attr.Class("space-y-2"),
								html.Div(
									attr.Class("flex items-center space-x-2 p-3 border rounded-lg bg-white hover:bg-muted/50 transition-all cursor-pointer"),
									html.Input(
										attr.Type("radio"),
										attr.Id("visibility-public"),
										attr.Name("visibility"),
										attr.Value("public"),
										html.If(data.Visibility == "public", attr.Checked()),
										attr.Class("h-4 w-4"),
									),
									html.Label(
										attr.For("visibility-public"),
										attr.Class("flex-1 cursor-pointer flex items-center gap-2"),
										ui.SVGIcon(ui.IconGlobe, "h-4 w-4 text-muted-foreground"),
										html.Div(
											attr.Class("flex flex-col"),
											html.Element("span",
												attr.Class("font-medium text-sm"),
												html.Text("Public"),
											),
											html.Element("span",
												attr.Class("text-xs text-muted-foreground"),
												html.Text("Anyone can view this repository"),
											),
										),
									),
								),
								html.Div(
									attr.Class("flex items-center space-x-2 p-3 border rounded-lg bg-white hover:bg-muted/50 transition-all cursor-pointer"),
									html.Input(
										attr.Type("radio"),
										attr.Id("visibility-private"),
										attr.Name("visibility"),
										attr.Value("private"),
										html.If(data.Visibility == "private", attr.Checked()),
										attr.Class("h-4 w-4"),
									),
									html.Label(
										attr.For("visibility-private"),
										attr.Class("flex-1 cursor-pointer flex items-center gap-2"),
										ui.SVGIcon(ui.IconLock, "h-4 w-4 text-muted-foreground"),
										html.Div(
											attr.Class("flex flex-col"),
											html.Element("span",
												attr.Class("font-medium text-sm"),
												html.Text("Private"),
											),
											html.Element("span",
												attr.Class("text-xs text-muted-foreground"),
												html.Text("Only you and collaborators can access"),
											),
										),
									),
								),
							),
						),
						html.Div(
							attr.Class("flex justify-end"),
							ui.Button(
								ui.ButtonProps{
									Variant: ui.ButtonPrimary,
									Type:    "submit",
								},
								html.Text("Save Changes"),
							),
						),
					),
				),
			}),

			// Collaborators Card
			ui.Card(ui.CardProps{
				Title:       "Collaborators",
				Description: "Manage repository access",
				Content: html.Div(
					attr.Class("space-y-4"),
					html.If(data.CollaboratorSuccess != "", html.Div(
						attr.Class("p-3 rounded-lg bg-emerald-50 dark:bg-emerald-900/20 border border-emerald-200 dark:border-emerald-800 text-emerald-800 dark:text-emerald-200 text-sm"),
						html.Text(data.CollaboratorSuccess),
					)),
					html.If(data.CollaboratorError != "", html.Div(
						attr.Class("p-3 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-800 dark:text-red-200 text-sm"),
						html.Text(data.CollaboratorError),
					)),
					// Collaborators List
					html.If(len(data.Collaborators) > 0, html.Div(
						attr.Class("space-y-2"),
						html.H3(
							attr.Class("text-sm font-medium mb-3"),
							html.Text("Current Collaborators"),
						),
						html.For(data.Collaborators, func(collab CollaboratorData) html.Node {
							return html.Div(
								attr.Class("flex flex-col sm:flex-row sm:items-center sm:justify-between p-4 border rounded-lg bg-white gap-4"),
								html.Div(
									attr.Class("flex items-center gap-3"),
									html.Div(
										attr.Class("flex items-center justify-center w-10 h-10 rounded-full bg-muted"),
										ui.SVGIcon(ui.IconUser, "h-5 w-5 text-muted-foreground"),
									),
									html.Div(
										attr.Class("flex flex-col"),
										html.Element("span",
											attr.Class("font-medium text-sm"),
											html.Text(collab.Username),
										),
										html.Element("span",
											attr.Class("text-xs text-muted-foreground flex items-center gap-1"),
											getRoleIcon(collab.Contributor.Role),
											html.Element("span",
												attr.Class("capitalize"),
												html.Text(collab.Contributor.Role+" access"),
											),
										),
									),
								),
								html.Div(
									attr.Class("flex items-center gap-2 sm:ml-auto"),
									// Update Role Form
									html.Form(
										attr.Method("POST"),
										attr.Action("/"+data.OwnerUsername+"/"+data.Repository.Name+"/settings/collaborators/update"),
										attr.Class("flex items-center gap-2"),
										html.Input(
											attr.Type("hidden"),
											attr.Name("user_id"),
											attr.Value(fmt.Sprintf("%d", collab.Contributor.UserID)),
										),
										ui.Select(ui.SelectProps{
											Id:    "role-" + fmt.Sprintf("%d", collab.Contributor.UserID),
											Name:  "role",
											Class: "!mb-0 w-32",
											Options: []ui.SelectOption{
												{Value: "read", Label: "Read", Selected: collab.Contributor.Role == "read", Icon: ui.IconEye},
												{Value: "write", Label: "Write", Selected: collab.Contributor.Role == "write", Icon: ui.IconEdit},
												{Value: "admin", Label: "Admin", Selected: collab.Contributor.Role == "admin", Icon: ui.IconShield},
											},
										}),
										ui.Button(
											ui.ButtonProps{
												Variant: ui.ButtonOutline,
												Type:    "submit",
											},
											html.Text("Update"),
										),
									),
									// Remove Collaborator Form
									html.Form(
										attr.Method("POST"),
										attr.Action("/"+data.OwnerUsername+"/"+data.Repository.Name+"/settings/collaborators/remove"),
										html.Input(
											attr.Type("hidden"),
											attr.Name("user_id"),
											attr.Value(fmt.Sprintf("%d", collab.Contributor.UserID)),
										),
										ui.Button(
											ui.ButtonProps{
												Variant: ui.ButtonDestructive,
												Type:    "submit",
											},
											html.Text("Remove"),
										),
									),
								),
							)
						}),
					)),
					html.If(len(data.Collaborators) == 0, html.Div(
						attr.Class("text-sm text-muted-foreground text-center py-8 border border-dashed rounded-lg"),
						html.Text("No collaborators yet. Add collaborators to give them access to this repository."),
					)),
					// Add Collaborator Form
					html.Div(
						attr.Class("mt-6 pt-6 border-t"),
						html.H3(
							attr.Class("text-sm font-medium mb-4"),
							html.Text("Add Collaborator"),
						),
						html.Form(
							attr.Method("POST"),
							attr.Action("/"+data.OwnerUsername+"/"+data.Repository.Name+"/settings/collaborators/add"),
							attr.Class("space-y-4"),
							html.Div(
								attr.Class("grid grid-cols-1 sm:grid-cols-[1fr_auto_auto] gap-4 justify-end items-end"),
								ui.FormField(ui.FormFieldProps{
									Label:       "Username",
									Id:          "collaborator-username",
									Name:        "username",
									Type:        "text",
									Placeholder: "Username",
									Icon:        ui.IconUser,
									Required:    true,
									Value:       data.NewCollaborator,
								}),
								ui.Select(ui.SelectProps{
									Id:       "collaborator-role",
									Name:     "role",
									Label:    "Role",
									Required: true,
									Class:    "sm:w-full !mb-0",
									Options: []ui.SelectOption{
										{Value: "read", Label: "Read", Selected: true, Icon: ui.IconEye},
										{Value: "write", Label: "Write", Icon: ui.IconEdit},
										{Value: "admin", Label: "Admin", Icon: ui.IconShield},
									},
								}),
								html.Div(
									attr.Class("flex items-end"),
									ui.Button(
										ui.ButtonProps{
											Variant: ui.ButtonPrimary,
											Type:    "submit",
										},
										html.Text("Add"),
									),
								),
							),
						),
					),
				),
			}),

			// Danger Zone Card
			ui.Card(ui.CardProps{
				Title:       "Danger Zone",
				Description: "Irreversible and destructive actions",
				Content: html.Div(
					attr.Class("space-y-4"),
					html.Div(
						attr.Class("flex items-center justify-between p-4 border border-destructive/50 rounded-lg bg-destructive/5"),
						html.Div(
							attr.Class("flex-1"),
							html.Element("h3",
								attr.Class("font-medium text-sm"),
								html.Text("Delete this repository"),
							),
							html.P(
								attr.Class("text-xs text-muted-foreground mt-1"),
								html.Text("Once you delete a repository, there is no going back. Please be certain."),
							),
						),
						ui.Button(
							ui.ButtonProps{
								Variant: ui.ButtonDestructive,
								OnClick: "confirmDeleteRepository()",
							},
							html.Text("Delete Repository"),
						),
					),
				),
			}),

			// JavaScript for name change confirmation and delete confirmation
			html.Element("script",
				html.Text(`
					(function() {
						const form = document.getElementById('general-settings-form');
						if (form) {
							form.addEventListener('submit', function(e) {
								const originalName = form.getAttribute('data-original-name');
								const currentName = document.getElementById('name').value;

								if (originalName !== currentName) {
									const confirmed = window.confirm(
										'Are you sure you want to rename this repository from "' + originalName + '" to "' + currentName + '"?\n\n' +
										'This will change the repository URL and may break existing clones.'
									);

									if (!confirmed) {
										e.preventDefault();
										return false;
									}
								}
							});
						}
					})();

					function confirmDeleteRepository() {
						const confirmed = window.confirm(
							'Are you ABSOLUTELY sure you want to delete this repository?\n\n' +
							'This action CANNOT be undone. This will permanently delete the repository, all commits, and all collaborators will lose access.\n\n' +
							'Type DELETE in the next prompt to confirm.'
						);

						if (confirmed) {
							const confirmation = window.prompt('Please type DELETE to confirm:');
							if (confirmation === 'DELETE') {
								const form = document.createElement('form');
								form.method = 'POST';
								form.action = window.location.pathname + '/delete';
								document.body.appendChild(form);
								form.submit();
							}
						}
					}
				`),
			),
		),
	)
}

func getRoleIcon(role string) html.Node {
	switch role {
	case "read":
		return ui.SVGIcon(ui.IconEye, "h-3 w-3")
	case "write":
		return ui.SVGIcon(ui.IconEdit, "h-3 w-3")
	case "admin":
		return ui.SVGIcon(ui.IconShield, "h-3 w-3")
	default:
		return html.Group()
	}
}