Skip to content
Open
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
5 changes: 5 additions & 0 deletions crates/api/examples/dashboard_e2e_seed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -905,6 +905,11 @@ async fn seed_projects_workspace_fixture(
short_description = EXCLUDED.short_description,
visibility = EXCLUDED.visibility,
default_repository_id = EXCLUDED.default_repository_id,
state = 'open',
closed_at = NULL,
closed_by_user_id = NULL,
deleted_at = NULL,
deleted_by_user_id = NULL,
updated_at = now()
RETURNING id
"#,
Expand Down
4 changes: 2 additions & 2 deletions crates/api/src/domain/milestones.rs
Original file line number Diff line number Diff line change
Expand Up @@ -817,8 +817,8 @@ async fn milestone_rows(
MilestoneSort::IssuesAsc => "total_count ASC, milestones.updated_at DESC",
};
let sql = format!(
"{} AND ($2::text IS NULL OR milestones.state = $2) ORDER BY {order_by} LIMIT $3 OFFSET $4",
milestone_select_sql("milestones.repository_id = $1")
"{} ORDER BY {order_by} LIMIT $3 OFFSET $4",
milestone_select_sql("milestones.repository_id = $1 AND ($2::text IS NULL OR milestones.state = $2)")
);
sqlx::query(&sql)
.bind(repository_id)
Expand Down
9 changes: 7 additions & 2 deletions crates/api/src/domain/projects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1607,12 +1607,17 @@ pub async fn copy_project_for_actor(
let owner: String = source
.try_get::<Option<String>, _>("owner_login")?
.unwrap_or_else(|| "unknown".to_owned());
let project_href = if owner_organization_id.is_some() {
format!("/orgs/{owner}/projects/{next_number}")
} else {
format!("/{owner}/projects/{next_number}")
};
Ok(CopiedProject {
id: new_project_id,
number: next_number,
title,
href: format!("/{owner}/projects/{next_number}"),
workspace_href: format!("/{owner}/projects/{next_number}/views/1"),
href: project_href.clone(),
workspace_href: format!("{project_href}/views/1"),
owner,
copied_views,
copied_fields,
Expand Down
4 changes: 2 additions & 2 deletions crates/api/tests/projects_list_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ async fn projects_lists_filter_templates_and_repository_links_without_leaking_pr
);
assert_eq!(
body["templates"]["items"][0]["projectHref"],
format!("/{marker}/projects/2/views/1")
format!("/orgs/{marker}/projects/2/views/1")
);

let copy_uri = format!("/api/projects/{public_project_id}/copies");
Expand All @@ -364,7 +364,7 @@ async fn projects_lists_filter_templates_and_repository_links_without_leaking_pr
assert_eq!(body["copiedDraftItems"], 1);
assert_eq!(
body["workspaceHref"],
format!("/{marker}/projects/4/views/1")
format!("/orgs/{marker}/projects/4/views/1")
);
let cloned_counts = sqlx::query(
r#"
Expand Down
2 changes: 1 addition & 1 deletion crates/api/tests/repository_code_overview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -579,7 +579,7 @@ async fn repository_header_actions_toggle_social_state_and_create_fork() {
assert!(fork_body["forkHref"]
.as_str()
.expect("fork href should exist")
.ends_with(&format!("/{}", repository.name)));
.ends_with(&format!("/{}-fork", repository.name)));
assert_eq!(fork_body["social"]["forksCount"], 1);
assert!(fork_body["social"]["forkedRepositoryHref"].is_string());

Expand Down
5 changes: 4 additions & 1 deletion crates/api/tests/repository_foundation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,10 @@ async fn user_owned_repositories_enforce_uniqueness_permissions_and_pagination()
.expect("owner should create own repository");

assert_eq!(repository.owner_user_id, Some(owner.id));
assert_eq!(repository.owner_login, owner.email);
assert_eq!(
repository.owner_login,
owner.email.split('@').next().unwrap()
);
assert_eq!(repository.visibility, RepositoryVisibility::Private);

let permission = repository_permission_for_user(&pool, repository.id, owner.id)
Expand Down
2 changes: 1 addition & 1 deletion crates/api/tests/repository_milestones_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ async fn repository_milestones_contract_lists_mutates_and_clears_associations()
"SELECT COUNT(*)::bigint FROM audit_events WHERE actor_user_id = $1 AND target_id = $2",
)
.bind(writer.id)
.bind(milestone.id)
.bind(milestone.id.to_string())
.fetch_one(&pool)
.await
.expect("audit count should load");
Expand Down
53 changes: 53 additions & 0 deletions web/src/app/[owner]/projects/[number]/settings/templates/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { AppShell } from "@/components/AppShell";
import { ProjectTemplateSettingsPage } from "@/components/ProjectTemplateSettingsPage";
import {
getSessionAndShellContext,
getUserProjectSettings,
} from "@/lib/server-session";

type UserProjectTemplateSettingsRouteProps = {
params: Promise<{ owner: string; number: string }>;
};

export default async function UserProjectTemplateSettingsRoute({
params,
}: UserProjectTemplateSettingsRouteProps) {
const [{ owner, number }, { session, shellContext }] = await Promise.all([
params,
getSessionAndShellContext(),
]);
const ownerLogin = decodeURIComponent(owner);
const projectNumber = Number.parseInt(number, 10);
const result = Number.isFinite(projectNumber)
? await getUserProjectSettings(ownerLogin, projectNumber)
: {
ok: false as const,
status: 404,
code: "not_found",
message: "Project template settings could not be found.",
};

return (
<AppShell session={session} shellContext={shellContext}>
{result.ok ? (
<ProjectTemplateSettingsPage
owner={ownerLogin}
scope="user"
settings={result.settings}
/>
) : (
<main className="mx-auto max-w-[760px] px-6 py-16">
<div className="card p-6">
<div className="t-label mb-2">Project templates unavailable</div>
<h1 className="t-h2">
This project template cannot be configured.
</h1>
<p className="t-sm mt-2" style={{ color: "var(--ink-3)" }}>
{result.message}
</p>
</div>
</main>
)}
</AppShell>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { AppShell } from "@/components/AppShell";
import { ProjectTemplateSettingsPage } from "@/components/ProjectTemplateSettingsPage";
import {
getOrganizationProjectSettings,
getSessionAndShellContext,
} from "@/lib/server-session";

type OrganizationProjectTemplateSettingsRouteProps = {
params: Promise<{ org: string; number: string }>;
};

export default async function OrganizationProjectTemplateSettingsRoute({
params,
}: OrganizationProjectTemplateSettingsRouteProps) {
const [{ org, number }, { session, shellContext }] = await Promise.all([
params,
getSessionAndShellContext(),
]);
const orgLogin = decodeURIComponent(org);
const projectNumber = Number.parseInt(number, 10);
const result = Number.isFinite(projectNumber)
? await getOrganizationProjectSettings(orgLogin, projectNumber)
: {
ok: false as const,
status: 404,
code: "not_found",
message: "Project template settings could not be found.",
};

return (
<AppShell session={session} shellContext={shellContext}>
{result.ok ? (
<ProjectTemplateSettingsPage
owner={orgLogin}
scope="organization"
settings={result.settings}
/>
) : (
<main className="mx-auto max-w-[760px] px-6 py-16">
<div className="card p-6">
<div className="t-label mb-2">Project templates unavailable</div>
<h1 className="t-h2">
This project template cannot be configured.
</h1>
<p className="t-sm mt-2" style={{ color: "var(--ink-3)" }}>
{result.message}
</p>
</div>
</main>
)}
</AppShell>
);
}
25 changes: 14 additions & 11 deletions web/src/components/ProjectSettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,12 @@ export function ProjectSettingsPage({
const canPublishStatus =
settings.viewerPermissions.canPublishStatus &&
settings.dangerState.state === "open";
const canLinkRepositories =
settings.viewerPermissions.canLinkRepositories &&
settings.dangerState.state === "open";
const canManageTemplate =
settings.viewerPermissions.canManageTemplate &&
settings.dangerState.state === "open";
const latestStatus = settings.statusUpdates[0] ?? null;
const visibilityReason =
settings.policy.visibilityLockedReason ??
Expand All @@ -149,7 +155,7 @@ export function ProjectSettingsPage({
{ key: "access", label: "Access", disabled: false },
{ key: "fields", label: "Fields", disabled: false },
{ key: "workflows", label: "Workflows", disabled: false },
{ key: "templates", label: "Templates", disabled: true },
{ key: "templates", label: "Templates", disabled: false },
{ key: "danger", label: "Danger Zone", disabled: false },
];

Expand Down Expand Up @@ -699,7 +705,7 @@ export function ProjectSettingsPage({

<section className="card" style={{ padding: 18 }}>
<div className="t-label">Repositories</div>
{canEditGeneral ? (
{canLinkRepositories ? (
<form
onSubmit={handleRepositoryLink}
style={{
Expand Down Expand Up @@ -753,7 +759,7 @@ export function ProjectSettingsPage({
{repository.isDefault ? (
<span className="chip active">Default</span>
) : null}
{canEditGeneral && !repository.isDefault ? (
{canLinkRepositories && !repository.isDefault ? (
<button
className="btn ghost sm"
disabled={
Expand Down Expand Up @@ -844,7 +850,7 @@ export function ProjectSettingsPage({
>
<input
defaultChecked={settings.template.isTemplate}
disabled={!settings.viewerPermissions.canManageTemplate}
disabled={!canManageTemplate}
name="isTemplate"
type="checkbox"
/>
Expand All @@ -859,7 +865,7 @@ export function ProjectSettingsPage({
defaultValue={
settings.template.title ?? settings.general.title
}
disabled={!settings.viewerPermissions.canManageTemplate}
disabled={!canManageTemplate}
id="template-title"
name="templateTitle"
style={{ display: "block", marginTop: 8, width: "100%" }}
Expand All @@ -872,7 +878,7 @@ export function ProjectSettingsPage({
<textarea
className="input"
defaultValue={settings.template.description ?? ""}
disabled={!settings.viewerPermissions.canManageTemplate}
disabled={!canManageTemplate}
id="template-description"
name="templateDescription"
rows={3}
Expand All @@ -890,18 +896,15 @@ export function ProjectSettingsPage({
>
<input
defaultChecked={settings.template.isPublic}
disabled={!settings.viewerPermissions.canManageTemplate}
disabled={!canManageTemplate}
name="isPublic"
type="checkbox"
/>
Allow copies from visible users
</label>
<button
className="btn primary"
disabled={
!settings.viewerPermissions.canManageTemplate ||
pendingAction === "template"
}
disabled={!canManageTemplate || pendingAction === "template"}
style={{ marginTop: 14 }}
type="submit"
>
Expand Down
Loading