Merge pull request 'Rework new repo dialog' (#6386) from fnetx/new-repo-form into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6386
Reviewed-by: 0ko <0ko@noreply.codeberg.org>
This commit is contained in:
Otto 2024-12-29 13:22:48 +00:00
commit 6d61ae5cbe
13 changed files with 489 additions and 304 deletions

View file

@ -1053,6 +1053,10 @@ admin.failed_to_replace_flags = Failed to replace repository flags
admin.flags_replaced = Repository flags replaced
new_repo_helper = A repository contains all project files, including revision history. Already hosting one elsewhere? <a href="%s">Migrate repository</a>.
new_from_template = Use a template
new_from_template_description = You can select an existing repository template on this instance and apply its settings.
new_advanced = Advanced settings
new_advanced_expand = Click to expand
owner = Owner
owner_helper = Some organizations may not show up in the dropdown due to a maximum repository count limit.
repo_name = Repository name
@ -1099,7 +1103,8 @@ object_format_helper = Object format of the repository. Cannot be changed later.
readme = README
readme_helper = Select a README file template
readme_helper_desc = This is the place where you can write a complete description for your project.
auto_init = Initialize repository (Adds .gitignore, License and README)
auto_init = Initialize repository
auto_init_description = Start the Git history with a README and optionally add License and .gitignore files.
create_repo = Create repository
default_branch = Default branch
default_branch_label = default

View file

@ -16,206 +16,34 @@
<p>{{ctx.Locale.TrN .MaxCreationLimit "repo.form.reach_limit_of_creation_1" "repo.form.reach_limit_of_creation_n" .MaxCreationLimit}}</p>
</div>
{{end}}
<div class="inline required field {{if .Err_Owner}}error{{end}}">
<label>{{ctx.Locale.Tr "repo.owner"}}</label>
<div class="ui selection owner dropdown">
<input type="hidden" id="uid" name="uid" value="{{.ContextUser.ID}}" required>
<span class="text truncated-item-container" title="{{.ContextUser.Name}}">
{{ctx.AvatarUtils.Avatar .ContextUser 28 "mini"}}
<span class="truncated-item-name">{{.ContextUser.ShortName 40}}</span>
</span>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu">
<div class="item truncated-item-container" data-value="{{.SignedUser.ID}}" title="{{.SignedUser.Name}}">
{{ctx.AvatarUtils.Avatar .SignedUser 28 "mini"}}
<span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span>
</div>
{{range .Orgs}}
<div class="item truncated-item-container" data-value="{{.ID}}" title="{{.Name}}">
{{ctx.AvatarUtils.Avatar . 28 "mini"}}
<span class="truncated-item-name">{{.ShortName 40}}</span>
</div>
{{end}}
</div>
</div>
<span class="help">{{ctx.Locale.Tr "repo.owner_helper"}}</span>
</div>
<fieldset>
{{template "repo/create_basic" .}}
</fieldset>
<div class="inline required field {{if .Err_RepoName}}error{{end}}">
<label for="repo_name">{{ctx.Locale.Tr "repo.repo_name"}}</label>
<input id="repo_name" name="repo_name" value="{{.repo_name}}" autofocus required maxlength="100">
<span class="help">{{ctx.Locale.Tr "repo.repo_name_helper"}}</span>
</div>
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.visibility"}}</label>
<div class="ui checkbox">
<input name="private" type="checkbox"
{{if .IsForcedPrivate}}
checked disabled
{{else}}
{{if .private}}checked{{end}}
{{end}}>
<label>{{ctx.Locale.Tr "repo.visibility_helper"}}</label>
</div>
{{if .IsForcedPrivate}}
<span class="help">{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</span>
{{end}}
<span class="help">{{ctx.Locale.Tr "repo.visibility_description"}}</span>
</div>
<div class="inline field {{if .Err_Description}}error{{end}}">
<label for="description">{{ctx.Locale.Tr "repo.repo_desc"}}</label>
<textarea id="description" rows="2" name="description" placeholder="{{ctx.Locale.Tr "repo.repo_desc_helper"}}" maxlength="2048">{{.description}}</textarea>
</div>
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.template"}}</label>
<div id="repo_template_search" class="ui search selection dropdown">
<input type="hidden" id="repo_template" name="repo_template" value="{{if ne .repo_template 0}}{{.repo_template}}{{end}}">
<div class="default text">{{.repo_template_name}}</div>
<div class="menu">
</div>
</div>
</div>
<div id="template_units" class="tw-hidden">
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.template.items"}}</label>
<div class="ui checkbox">
<input name="git_content" type="checkbox" {{if .git_content}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.template.git_content"}}</label>
</div>
<div class="ui checkbox" {{if not .SignedUser.CanEditGitHook}}data-tooltip-content="{{ctx.Locale.Tr "repo.template.git_hooks_tooltip"}}"{{end}}>
<input name="git_hooks" type="checkbox" {{if .git_hooks}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.template.git_hooks"}}</label>
</div>
</div>
<div class="inline field">
<label></label>
<div class="ui checkbox">
<input name="webhooks" type="checkbox" {{if .webhooks}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.template.webhooks"}}</label>
</div>
<div class="ui checkbox">
<input name="topics" type="checkbox" {{if .topics}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.template.topics"}}</label>
</div>
</div>
<div class="inline field">
<label></label>
<div class="ui checkbox">
<input name="avatar" type="checkbox" {{if .avatar}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.template.avatar"}}</label>
</div>
<div class="ui checkbox">
<input name="labels" type="checkbox" {{if .labels}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.template.issue_labels"}}</label>
</div>
</div>
<div class="inline field">
<label></label>
<div class="ui checkbox">
<input name="protected_branch" type="checkbox" {{if .protected_branch}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.protected_branch"}}</label>
</div>
</div>
</div>
<fieldset>
<legend>
{{ctx.Locale.Tr "repo.new_from_template"}}
<span class="help">{{ctx.Locale.Tr "repo.new_from_template_description"}}</span>
</legend>
{{template "repo/create_from_template" .}}
</fieldset>
<div id="non_template">
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.issue_labels"}}</label>
<div class="ui search selection dropdown">
<input type="hidden" name="issue_labels" value="{{.issueLabels}}">
<div class="default text">{{ctx.Locale.Tr "repo.issue_labels_helper"}}</div>
<div class="menu">
<div class="item" data-value="">{{ctx.Locale.Tr "repo.issue_labels_helper"}}</div>
{{range .LabelTemplateFiles}}
<div class="item" data-value="{{.DisplayName}}">{{.DisplayName}}<br><p>({{.Description}})</p></div>
{{end}}
</div>
</div>
</div>
<fieldset>
<legend>{{ctx.Locale.Tr "repo.auto_init"}}</legend>
{{template "repo/create_init" .}}
</fieldset>
<div class="divider"></div>
<div class="inline field">
<label>.gitignore</label>
<div class="ui multiple search selection dropdown">
<input type="hidden" name="gitignores" value="{{.gitignores}}">
<div class="default text">{{ctx.Locale.Tr "repo.repo_gitignore_helper"}}</div>
<div class="menu">
{{range .Gitignores}}
<div class="item" data-value="{{.}}">{{.}}</div>
{{end}}
</div>
</div>
<span class="help">{{ctx.Locale.Tr "repo.repo_gitignore_helper_desc"}}</span>
</div>
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.license"}}</label>
<div class="ui search selection dropdown">
<input type="hidden" name="license" value="{{.license}}">
<div class="default text">{{ctx.Locale.Tr "repo.license_helper"}}</div>
<div class="menu">
<div class="item" data-value="">{{ctx.Locale.Tr "repo.license_helper"}}</div>
{{range .Licenses}}
<div class="item" data-value="{{.}}">{{.}}</div>
{{end}}
</div>
</div>
<span class="help">{{ctx.Locale.Tr "repo.license_helper_desc" "https://choosealicense.com/"}}</span>
</div>
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.readme"}}</label>
<div class="ui selection dropdown">
<input type="hidden" name="readme" value="{{.readme}}">
<div class="default text">{{ctx.Locale.Tr "repo.readme_helper"}}</div>
<div class="menu">
{{range .Readmes}}
<div class="item" data-value="{{.}}">{{.}}</div>
{{end}}
</div>
</div>
<span class="help">{{ctx.Locale.Tr "repo.readme_helper_desc"}}</span>
</div>
<div class="inline field">
<div class="ui checkbox" id="auto-init">
<input name="auto_init" type="checkbox" {{if .auto_init}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.auto_init"}}</label>
</div>
</div>
<div class="inline field">
<label for="default_branch">{{ctx.Locale.Tr "repo.default_branch"}}</label>
<input id="default_branch" name="default_branch" value="{{.default_branch}}" placeholder="{{.default_branch}}">
<span class="help">{{ctx.Locale.Tr "repo.default_branch_helper"}}</span>
</div>
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.object_format"}}</label>
<div class="ui selection owner dropdown">
<input type="hidden" id="object_format_name" name="object_format_name" value="{{.DefaultObjectFormat.Name}}" required>
<div class="default text">{{.DefaultObjectFormat.Name}}</div>
<div class="menu">
{{range .SupportedObjectFormats}}
<div class="item" data-value="{{.Name}}">{{.Name}}</div>
{{end}}
</div>
</div>
<span class="help">{{ctx.Locale.Tr "repo.object_format_helper"}}</span>
</div>
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.template"}}</label>
<div class="ui checkbox">
<input name="template" type="checkbox">
<label>{{ctx.Locale.Tr "repo.template_helper"}}</label>
</div>
</div>
</div>
<br>
<div class="inline field">
<label></label>
<button class="ui primary button{{if not .CanCreateRepo}} disabled{{end}}">
{{ctx.Locale.Tr "repo.create_repo"}}
</button>
<fieldset>
<legend>{{ctx.Locale.Tr "repo.new_advanced"}}</legend>
<details><summary>{{ctx.Locale.Tr "repo.new_advanced_expand"}}</summary>
{{template "repo/create_advanced" .}}
</details>
</fieldset>
</div>
<button class="ui primary button{{if not .CanCreateRepo}} disabled{{end}}">
{{ctx.Locale.Tr "repo.create_repo"}}
</button>
</div>
</form>
</div>

View file

@ -0,0 +1,45 @@
<label>
{{ctx.Locale.Tr "repo.issue_labels"}}
<div class="ui search selection dropdown">
<input type="hidden" name="issue_labels" value="{{.issueLabels}}">
<div class="default text">{{ctx.Locale.Tr "repo.issue_labels_helper"}}</div>
<div class="menu">
<div class="item" data-value="">{{ctx.Locale.Tr "repo.issue_labels_helper"}}</div>
{{range .LabelTemplateFiles}}
<div class="item" data-value="{{.DisplayName}}">{{.DisplayName}}<br><p>({{.Description}})</p></div>
{{end}}
</div>
</div>
</label>
{{$supportedFormatsLength := len .SupportedObjectFormats}}
{{/* Only offer object format selection if there is an actual choice */}}
{{if ge $supportedFormatsLength 2}}
<label>
{{ctx.Locale.Tr "repo.object_format"}}
<div class="ui selection dropdown">
<input type="hidden" id="object_format_name" name="object_format_name" value="{{.DefaultObjectFormat.Name}}" required>
<div class="default text">{{.DefaultObjectFormat.Name}}</div>
<div class="menu">
{{range .SupportedObjectFormats}}
<div class="item" data-value="{{.Name}}">{{.Name}}</div>
{{end}}
</div>
</div>
<span class="help">{{ctx.Locale.Tr "repo.object_format_helper"}}</span>
</label>
{{else}}
<input type="hidden" name="object_format_name" value="{{.DefaultObjectFormat.Name}}" required>
{{end}}
<label>
{{ctx.Locale.Tr "repo.default_branch"}}
<input name="default_branch" value="{{.default_branch}}" placeholder="{{.default_branch}}">
<span class="help">{{ctx.Locale.Tr "repo.default_branch_helper"}}</span>
</label>
<label>
<input name="template" type="checkbox">
{{ctx.Locale.Tr "repo.template_helper"}}
<span class="help">{{ctx.Locale.Tr "repo.template_description"}}</span>
</label>

View file

@ -0,0 +1,47 @@
<label id="repo_owner_label" {{if .Err_Owner}}class="field error"{{end}}>
{{ctx.Locale.Tr "repo.owner"}}
<div class="ui selection required dropdown" aria-labelledby="repo_owner_label">
{{/* uid id is used by the repo-template code */}}
<input type="hidden" id="uid" name="uid" value="{{.ContextUser.ID}}" required>
<span class="text truncated-item-container" title="{{.ContextUser.Name}}">
{{ctx.AvatarUtils.Avatar .ContextUser 28 "mini"}}
<span class="truncated-item-name">{{.ContextUser.ShortName 40}}</span>
</span>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu">
<div class="item truncated-item-container" data-value="{{.SignedUser.ID}}" title="{{.SignedUser.Name}}">
{{ctx.AvatarUtils.Avatar .SignedUser 28 "mini"}}
<span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span>
</div>
{{range .Orgs}}
<div class="item truncated-item-container" data-value="{{.ID}}" title="{{.Name}}">
{{ctx.AvatarUtils.Avatar . 28 "mini"}}
<span class="truncated-item-name">{{.ShortName 40}}</span>
</div>
{{end}}
</div>
</div>
<span class="help">{{ctx.Locale.Tr "repo.owner_helper"}}</span>
</label>
<label {{if .Err_RepoName}}class="field error"{{end}}>
{{ctx.Locale.Tr "repo.repo_name"}}
<input name="repo_name" value="{{.repo_name}}" required maxlength="100">
<span class="help">{{ctx.Locale.Tr "repo.repo_name_helper"}}</span>
</label>
<label>
<input name="private" type="checkbox"
{{if .IsForcedPrivate}}
checked disabled
{{else}}
{{if .private}}checked{{end}}
{{end}}>
{{ctx.Locale.Tr "repo.visibility_helper"}}
{{if .IsForcedPrivate}}
<span class="help">{{ctx.Locale.Tr "repo.visibility_helper_forced"}}</span>
{{end}}
<span class="help">{{ctx.Locale.Tr "repo.visibility_description"}}</span>
</label>
<label {{if .Err_Description}}class="field error"{{end}}>
{{ctx.Locale.Tr "repo.repo_desc"}}
<textarea rows="2" name="description" placeholder="{{ctx.Locale.Tr "repo.repo_desc_helper"}}" maxlength="2048">{{.description}}</textarea>
</label>

View file

@ -0,0 +1,49 @@
<label class="tw-mb-0">
{{ctx.Locale.Tr "repo.template"}}
</label>
{{/* If the dropdown is inside the label, the focus works correctly and it is more accessible.
However, the Javascript takes the focus and opens the dropdown again immediately after closing.
When the user interacts (via mouse or keyboard), the dropdown closes again.
Due to the fieldset legend, this solutions is probably acceptable until the dropdown can be fixed properly. */}}
<div id="repo_template_search" class="ui search selection dropdown tw-w-full">
<input type="hidden" id="repo_template" name="repo_template" value="{{if ne .repo_template 0}}{{.repo_template}}{{end}}">
<div class="default text">{{.repo_template_name}}</div>
<div class="menu">
</div>
</div>
<fieldset id="template_units" class="tw-hidden simple-grid grid-2">
<legend>{{ctx.Locale.Tr "repo.template.items"}}</legend>
<label>
<input name="git_content" type="checkbox" {{if .git_content}}checked{{end}}>
{{ctx.Locale.Tr "repo.template.git_content"}}
</label>
<label>
<input name="webhooks" type="checkbox" {{if .webhooks}}checked{{end}}>
{{ctx.Locale.Tr "repo.template.webhooks"}}
</label>
<label>
<input name="topics" type="checkbox" {{if .topics}}checked{{end}}>
{{ctx.Locale.Tr "repo.template.topics"}}
</label>
<label>
<input name="avatar" type="checkbox" {{if .avatar}}checked{{end}}>
{{ctx.Locale.Tr "repo.template.avatar"}}
</label>
<label>
<input name="labels" type="checkbox" {{if .labels}}checked{{end}}>
{{ctx.Locale.Tr "repo.template.issue_labels"}}
</label>
<label>
<input name="protected_branch" type="checkbox" {{if .protected_branch}}checked{{end}}>
{{ctx.Locale.Tr "repo.settings.protected_branch"}}
</label>
<label>
<input name="git_hooks" type="checkbox" {{if .git_hooks}}checked{{end}}>
{{ctx.Locale.Tr "repo.template.git_hooks"}}
{{if not .SignedUser.CanEditGitHook}}
<span class="help">{{ctx.Locale.Tr "repo.template.git_hooks_tooltip"}}</span>
{{end}}
</label>
</fieldset>

View file

@ -0,0 +1,56 @@
<label>
<input name="auto_init" type="checkbox" {{if .auto_init}}checked{{end}}>
{{ctx.Locale.Tr "repo.auto_init"}}
<span class="help">{{ctx.Locale.Tr "repo.auto_init_description"}}</span>
</label>
<div class="hide-unless-checked">
<label>
.gitignore
<div class="ui multiple search selection dropdown">
<input type="hidden" name="gitignores" value="{{.gitignores}}">
<div class="default text">{{ctx.Locale.Tr "repo.repo_gitignore_helper"}}</div>
<div class="menu">
{{range .Gitignores}}
<div class="item" data-value="{{.}}">{{.}}</div>
{{end}}
</div>
</div>
<span class="help">{{ctx.Locale.Tr "repo.repo_gitignore_helper_desc"}}</span>
</label>
<label>
{{ctx.Locale.Tr "repo.license"}}
<div class="ui search selection dropdown">
<input type="hidden" name="license" value="{{.license}}">
<div class="default text">{{ctx.Locale.Tr "repo.license_helper"}}</div>
<div class="menu">
<div class="item" data-value="">{{ctx.Locale.Tr "repo.license_helper"}}</div>
{{range .Licenses}}
<div class="item" data-value="{{.}}">{{.}}</div>
{{end}}
</div>
</div>
<span class="help">{{ctx.Locale.Tr "repo.license_helper_desc" "https://choosealicense.com/"}}</span>
</label>
{{$supportedReadmesLength := len .Readmes}}
{{/* Only offer README selection if there is an actual choice */}}
{{if ge $supportedReadmesLength 2}}
<label>
{{ctx.Locale.Tr "repo.readme"}}
<div class="ui selection dropdown">
<input type="hidden" name="readme" value="{{.readme}}">
<div class="default text">{{ctx.Locale.Tr "repo.readme_helper"}}</div>
<div class="menu">
{{range .Readmes}}
<div class="item" data-value="{{.}}">{{.}}</div>
{{end}}
</div>
</div>
<span class="help">{{ctx.Locale.Tr "repo.readme_helper_desc"}}</span>
</label>
{{else}}
<input type="hidden" name="readme" value="Default">
{{end}}
</div>

View file

@ -0,0 +1,134 @@
// @watch start
// templates/repo/create**.tmpl
// web_src/css/{form,repo}.css
// @watch end
import {expect} from '@playwright/test';
import {test, dynamic_id, save_visual, login_user, login} from './utils_e2e.ts';
import {validate_form} from './shared/forms.ts';
test.beforeAll(async ({browser}, workerInfo) => {
await login_user(browser, workerInfo, 'user2');
});
test('New repo: invalid', async ({browser}, workerInfo) => {
const page = await login({browser}, workerInfo);
const response = await page.goto('/repo/create');
expect(response?.status()).toBe(200);
// check that relevant form content is hidden or available
await expect(page.getByRole('group', {name: 'Use a template You can select'}).getByRole('combobox')).toBeVisible();
await expect(page.getByText('.gitignore Select .gitignore')).toBeHidden();
await expect(page.getByText('Labels Select a label set')).toBeHidden();
await validate_form({page}, 'fieldset');
await save_visual(page);
await page.getByLabel('Repository name').fill('*invalid');
await page.getByRole('button', {name: 'Create repository'}).click();
await expect(page.getByText('Repository name should contain only alphanumeric')).toBeVisible();
await save_visual(page);
});
test('New repo: initialize', async ({browser}, workerInfo) => {
const page = await login({browser}, workerInfo);
const response = await page.goto('/repo/create');
expect(response?.status()).toBe(200);
// check that relevant form content is hidden or available
await expect(page.getByRole('group', {name: 'Use a template You can select'}).getByRole('combobox')).toBeVisible();
await expect(page.getByText('.gitignore Select .gitignore')).toBeHidden();
// fill initialization section
await page.getByText('Start the Git history with').click();
await page.getByText('Select .gitignore templates').click();
await page.getByLabel('.gitignore Select .gitignore').fill('Go');
await page.getByRole('option', {name: 'Go', exact: true}).click();
await page.keyboard.press('Escape');
await page.getByLabel('License Select a license file').click();
await page.getByRole('option', {name: 'MIT', exact: true}).click();
await page.keyboard.press('Escape');
// add advanced settings
await page.getByText('Click to expand').click();
await page.getByPlaceholder('master').fill('main');
await page.getByLabel('Make repository a template').check();
await validate_form({page}, 'fieldset');
await save_visual(page);
const reponame = dynamic_id();
await page.getByLabel('Repository name').fill(reponame);
await page.getByRole('button', {name: 'Create repository'}).click();
await expect(page.getByRole('link', {name: '.gitignore'})).toBeVisible();
await expect(page.getByRole('link', {name: 'LICENSE', exact: true})).toBeVisible();
if (!workerInfo.project.name.includes('Mobile')) {
await expect(page.getByText('Template', {exact: true})).toBeVisible();
}
await save_visual(page);
});
test('New repo: initialize later', async ({browser}, workerInfo) => {
const page = await login({browser}, workerInfo);
const response = await page.goto('/repo/create');
expect(response?.status()).toBe(200);
const reponame = dynamic_id();
await page.getByLabel('Repository name').fill(reponame);
await page.getByPlaceholder('Enter short description').fill(`Description for repo ${reponame}`);
await page.getByText('Click to expand').click();
await page.getByPlaceholder('master').fill('devbranch');
await validate_form({page}, 'fieldset');
await page.getByRole('button', {name: 'Create repository'}).click();
expect(page.url()).toBe(`http://localhost:3003/user2/${reponame}`);
await expect(page.getByRole('link', {name: 'New file'})).toBeVisible();
await expect(page.getByRole('heading', {name: 'Creating a new repository on'})).toBeVisible();
await save_visual(page);
// add a README
await page.getByRole('link', {name: 'New file'}).click();
// wait for loading spinner to disappear
// Otherwise, filling the filename might not populate the tree_path form field or preview tab
// The editor has race conditions, likely related to https://codeberg.org/forgejo/forgejo/issues/3371
await expect(page.locator('.is-loading')).toBeHidden();
await page.locator('.view-lines').click();
await page.keyboard.type('# Heading\n\nHello Forgejo!');
await page.getByPlaceholder('Name your file…').fill('README.md');
await expect(page.getByText('Preview')).toBeVisible();
await page.getByPlaceholder('Add "<filename>"').fill('My first commit message');
await page.getByRole('button', {name: 'Commit changes'}).click();
expect(page.url()).toBe(`http://localhost:3003/user2/${reponame}/src/branch/devbranch/README.md`);
await expect(page.getByRole('link', {name: 'My first commit message'})).toBeVisible();
await expect(page.getByText('Hello Forgejo!')).toBeVisible();
await save_visual(page);
});
test('New repo: from template', async ({browser}, workerInfo) => {
test.skip(['Mobile Safari', 'webkit'].includes(workerInfo.project.name), 'WebKit browsers seem to have CORS issues with localhost here.');
const page = await login({browser}, workerInfo);
const response = await page.goto('/repo/create');
expect(response?.status()).toBe(200);
const reponame = dynamic_id();
await page.getByRole('group', {name: 'Use a template You can select'}).getByRole('combobox').click();
await page.getByRole('option', {name: 'user27/template1'}).click();
await page.getByText('Git content (Default branch)').click();
await save_visual(page);
await page.getByLabel('Repository name').fill(reponame);
await page.getByRole('button', {name: 'Create repository'}).click();
await expect(page.getByRole('link', {name: `${reponame}.log`})).toBeVisible();
await save_visual(page);
});
test('New repo: label set', async ({browser}, workerInfo) => {
const page = await login({browser}, workerInfo);
await page.goto('/repo/create');
const reponame = dynamic_id();
await page.getByText('Click to expand').click();
await page.getByLabel('Labels Select a label set').click();
await page.getByRole('option', {name: 'Advanced (Kind/Bug, Kind/'}).click();
// close dropdown via unrelated click
await page.getByText('You can select an existing').click();
await save_visual(page);
await page.getByLabel('Repository name').fill(reponame);
await page.getByRole('button', {name: 'Create repository'}).click();
await page.goto(`/user2/${reponame}/issues`);
await page.getByRole('link', {name: 'Labels'}).click();
await expect(page.getByText('Kind/Bug Something is not')).toBeVisible();
await save_visual(page);
});

View file

@ -7,6 +7,9 @@ export async function validate_form({page}: {page: Page}, scope: 'form' | 'field
'span[data-tooltip-content',
// exclude weird non-semantic HTML disabled content
'.disabled',
// legacy dropdowns don't use semantic HTML yet,
// avoid using these where possible
'.ui.dropdown',
];
await accessibilityCheck({page}, [scope], excludedElements, []);

View file

@ -81,6 +81,14 @@ export async function save_visual(page: Page) {
await page.locator('.flex-item-body > relative-time').filter({hasText: /now|minute/}).evaluateAll((nodes) => {
for (const node of nodes) node.outerHTML = 'relative time in repo';
});
// dynamically generated UUIDs
await page.getByText('dyn-id-').evaluateAll((nodes) => {
for (const node of nodes) node.innerHTML = node.innerHTML.replaceAll(/dyn-id-[a-f0-9-]+/g, 'dynamic-id');
});
// repeat above, work around https://github.com/microsoft/playwright/issues/34152
await page.getByText('dyn-id-').evaluateAll((nodes) => {
for (const node of nodes) node.innerHTML = node.innerHTML.replaceAll(/dyn-id-[a-f0-9-]+/g, 'dynamic-id');
});
await page.locator('relative-time').evaluateAll((nodes) => {
for (const node of nodes) node.outerHTML = 'time element';
});
@ -97,6 +105,8 @@ export async function save_visual(page: Page) {
page.locator('#repo_migrating'),
// update order of recently created repos is not fully deterministic
page.locator('.flex-item-main').filter({hasText: 'relative time in repo'}),
// dynamic IDs in fixed-size inputs
page.locator('input[value*="dyn-id-"]'),
],
});
}
@ -122,3 +132,8 @@ export async function create_temp_user(browser: Browser, workerInfo: TestInfo, r
return {context: await login_user(browser, workerInfo, username), username};
}
// returns a random string with a pattern that can be filtered for screenshots automatically
export function dynamic_id() {
return `dyn-id-${globalThis.crypto.randomUUID()}`;
}

View file

@ -43,7 +43,7 @@ func assertRepoCreateForm(t *testing.T, htmlDoc *HTMLDoc, owner *user_model.User
// the template menu is loaded client-side, so don't assert the option exists
assert.Equal(t, templateID, htmlDoc.GetInputValueByName("repo_template"), "Unexpected repo_template selection")
for _, name := range []string{"issue_labels", "gitignores", "license", "readme", "object_format_name"} {
for _, name := range []string{"issue_labels", "gitignores", "license", "object_format_name"} {
htmlDoc.AssertDropdownHasOptions(t, name)
}
}

View file

@ -652,97 +652,6 @@ img.ui.avatar,
background: var(--color-active);
}
.ui.form .fields.error .field textarea,
.ui.form .fields.error .field select,
.ui.form .fields.error .field input:not([type]),
.ui.form .fields.error .field input[type="date"],
.ui.form .fields.error .field input[type="datetime-local"],
.ui.form .fields.error .field input[type="email"],
.ui.form .fields.error .field input[type="number"],
.ui.form .fields.error .field input[type="password"],
.ui.form .fields.error .field input[type="search"],
.ui.form .fields.error .field input[type="tel"],
.ui.form .fields.error .field input[type="time"],
.ui.form .fields.error .field input[type="text"],
.ui.form .fields.error .field input[type="file"],
.ui.form .fields.error .field input[type="url"],
.ui.form .fields.error .field .ui.dropdown,
.ui.form .fields.error .field .ui.dropdown .item,
.ui.form .field.error .ui.dropdown,
.ui.form .field.error .ui.dropdown .text,
.ui.form .field.error .ui.dropdown .item,
.ui.form .field.error textarea,
.ui.form .field.error select,
.ui.form .field.error input:not([type]),
.ui.form .field.error input[type="date"],
.ui.form .field.error input[type="datetime-local"],
.ui.form .field.error input[type="email"],
.ui.form .field.error input[type="number"],
.ui.form .field.error input[type="password"],
.ui.form .field.error input[type="search"],
.ui.form .field.error input[type="tel"],
.ui.form .field.error input[type="time"],
.ui.form .field.error input[type="text"],
.ui.form .field.error input[type="file"],
.ui.form .field.error input[type="url"],
.ui.form .field.error select:focus,
.ui.form .field.error input:not([type]):focus,
.ui.form .field.error input[type="date"]:focus,
.ui.form .field.error input[type="datetime-local"]:focus,
.ui.form .field.error input[type="email"]:focus,
.ui.form .field.error input[type="number"]:focus,
.ui.form .field.error input[type="password"]:focus,
.ui.form .field.error input[type="search"]:focus,
.ui.form .field.error input[type="tel"]:focus,
.ui.form .field.error input[type="time"]:focus,
.ui.form .field.error input[type="text"]:focus,
.ui.form .field.error input[type="file"]:focus,
.ui.form .field.error input[type="url"]:focus {
background-color: var(--color-error-bg);
border-color: var(--color-error-border);
color: var(--color-error-text);
}
.ui.form .fields.error .field .ui.dropdown,
.ui.form .field.error .ui.dropdown,
.ui.form .fields.error .field .ui.dropdown:hover,
.ui.form .field.error .ui.dropdown:hover {
border-color: var(--color-error-border) !important;
}
.ui.form .fields.error .field .ui.dropdown .menu .item:hover,
.ui.form .field.error .ui.dropdown .menu .item:hover {
background-color: var(--color-error-bg-hover);
}
.ui.form .fields.error .field .ui.dropdown .menu .active.item,
.ui.form .field.error .ui.dropdown .menu .active.item {
background-color: var(--color-error-bg-active) !important;
}
.ui.form .fields.error .dropdown .menu,
.ui.form .field.error .dropdown .menu {
border-color: var(--color-error-border) !important;
}
input:-webkit-autofill,
input:-webkit-autofill:focus,
input:-webkit-autofill:hover,
input:-webkit-autofill:active,
.ui.form .field.field input:-webkit-autofill,
.ui.form .field.field input:-webkit-autofill:focus,
.ui.form .field.field input:-webkit-autofill:hover,
.ui.form .field.field input:-webkit-autofill:active {
-webkit-background-clip: text;
-webkit-text-fill-color: var(--color-text);
box-shadow: 0 0 0 100px var(--color-primary-light-6) inset !important;
border-color: var(--color-primary-light-4) !important;
}
.ui.form .field.muted {
opacity: var(--opacity-disabled);
}
.text.primary {
color: var(--color-primary) !important;
}

View file

@ -18,6 +18,11 @@ fieldset label:has(input[type="number"]) {
font-weight: var(--font-weight-medium);
}
/* override inline style on custom input elements */
fieldset label .ui.dropdown {
width: 100% !important;
}
fieldset .help {
font-weight: var(--font-weight-normal);
}
@ -27,9 +32,17 @@ fieldset .help {
padding-bottom: 0;
}
fieldset input[type="checkbox"],
fieldset input[type="radio"] {
fieldset label > input,
fieldset label > textarea,
fieldset label > .ui.dropdown,
fieldset label + .ui.dropdown {
margin-top: 0.28rem !important;
}
fieldset label > input[type="checkbox"],
fieldset label > input[type="radio"] {
margin-right: 0.75em;
margin-top: 0 !important;
vertical-align: initial !important; /* overrides a semantic.css rule, remove when obsolete */
}
@ -142,6 +155,101 @@ textarea:focus,
color: var(--color-input-text);
}
/* error messages */
fieldset label.error textarea,
fieldset label.error select,
fieldset label.error input,
.ui.form .fields.error .field textarea,
.ui.form .fields.error .field select,
.ui.form .fields.error .field input:not([type]),
.ui.form .fields.error .field input[type="date"],
.ui.form .fields.error .field input[type="datetime-local"],
.ui.form .fields.error .field input[type="email"],
.ui.form .fields.error .field input[type="number"],
.ui.form .fields.error .field input[type="password"],
.ui.form .fields.error .field input[type="search"],
.ui.form .fields.error .field input[type="tel"],
.ui.form .fields.error .field input[type="time"],
.ui.form .fields.error .field input[type="text"],
.ui.form .fields.error .field input[type="file"],
.ui.form .fields.error .field input[type="url"],
.ui.form .fields.error .field .ui.dropdown,
.ui.form .fields.error .field .ui.dropdown .item,
.ui.form .field.error .ui.dropdown,
.ui.form .field.error .ui.dropdown .text,
.ui.form .field.error .ui.dropdown .item,
.ui.form .field.error textarea,
.ui.form .field.error select,
.ui.form .field.error input:not([type]),
.ui.form .field.error input[type="date"],
.ui.form .field.error input[type="datetime-local"],
.ui.form .field.error input[type="email"],
.ui.form .field.error input[type="number"],
.ui.form .field.error input[type="password"],
.ui.form .field.error input[type="search"],
.ui.form .field.error input[type="tel"],
.ui.form .field.error input[type="time"],
.ui.form .field.error input[type="text"],
.ui.form .field.error input[type="file"],
.ui.form .field.error input[type="url"],
.ui.form .field.error select:focus,
.ui.form .field.error input:not([type]):focus,
.ui.form .field.error input[type="date"]:focus,
.ui.form .field.error input[type="datetime-local"]:focus,
.ui.form .field.error input[type="email"]:focus,
.ui.form .field.error input[type="number"]:focus,
.ui.form .field.error input[type="password"]:focus,
.ui.form .field.error input[type="search"]:focus,
.ui.form .field.error input[type="tel"]:focus,
.ui.form .field.error input[type="time"]:focus,
.ui.form .field.error input[type="text"]:focus,
.ui.form .field.error input[type="file"]:focus,
.ui.form .field.error input[type="url"]:focus {
background-color: var(--color-error-bg);
border-color: var(--color-error-border);
color: var(--color-error-text);
}
.ui.form .fields.error .field .ui.dropdown,
.ui.form .field.error .ui.dropdown,
.ui.form .fields.error .field .ui.dropdown:hover,
.ui.form .field.error .ui.dropdown:hover {
border-color: var(--color-error-border) !important;
}
.ui.form .fields.error .field .ui.dropdown .menu .item:hover,
.ui.form .field.error .ui.dropdown .menu .item:hover {
background-color: var(--color-error-bg-hover);
}
.ui.form .fields.error .field .ui.dropdown .menu .active.item,
.ui.form .field.error .ui.dropdown .menu .active.item {
background-color: var(--color-error-bg-active) !important;
}
.ui.form .fields.error .dropdown .menu,
.ui.form .field.error .dropdown .menu {
border-color: var(--color-error-border) !important;
}
input:-webkit-autofill,
input:-webkit-autofill:focus,
input:-webkit-autofill:hover,
input:-webkit-autofill:active,
.ui.form .field.field input:-webkit-autofill,
.ui.form .field.field input:-webkit-autofill:focus,
.ui.form .field.field input:-webkit-autofill:hover,
.ui.form .field.field input:-webkit-autofill:active {
-webkit-background-clip: text;
-webkit-text-fill-color: var(--color-text);
box-shadow: 0 0 0 100px var(--color-primary-light-6) inset !important;
border-color: var(--color-primary-light-4) !important;
}
.ui.form .field.muted {
opacity: var(--opacity-disabled);
}
.ui.form .field > label,
.ui.form .inline.fields > label,
.ui.form .inline.fields .field > label,
@ -400,14 +508,12 @@ textarea:focus,
.repository.new.fork form .header {
padding-left: 280px !important;
}
.repository.new.repo form .inline.field > label,
.repository.new.migrate form .inline.field > label,
.repository.new.fork form .inline.field > label {
text-align: right;
width: 250px !important;
word-wrap: break-word;
}
.repository.new.repo form .help,
.repository.new.migrate form .help,
.repository.new.fork form .help {
margin-left: 265px !important;
@ -417,10 +523,8 @@ textarea:focus,
.repository.new.fork form .optional .title {
margin-left: 250px !important;
}
.repository.new.repo form .inline.field > input,
.repository.new.migrate form .inline.field > input,
.repository.new.fork form .inline.field > input,
.repository.new.repo form .inline.field > textarea,
.repository.new.migrate form .inline.field > textarea,
.repository.new.fork form .inline.field > textarea {
width: 50%;
@ -440,7 +544,6 @@ textarea:focus,
}
}
.repository.new.repo form .dropdown .text,
.repository.new.migrate form .dropdown .text,
.repository.new.fork form .dropdown .text {
margin-right: 0 !important;
@ -453,7 +556,6 @@ textarea:focus,
text-align: center;
}
.repository.new.repo form .selection.dropdown,
.repository.new.migrate form .selection.dropdown,
.repository.new.fork form .selection.dropdown,
.repository.new.fork form .field a {
@ -490,10 +592,6 @@ textarea:focus,
}
}
.repository.new.repo .ui.form .selection.dropdown:not(.owner) {
width: 50% !important;
}
@media (max-width: 767.98px) {
.repository.new.repo .ui.form .selection.dropdown:not(.owner) {
width: 100% !important;

View file

@ -4,10 +4,6 @@
user-select: none;
}
.repository .owner.dropdown {
min-width: 40% !important;
}
.repository .unicode-escaped .escaped-code-point[data-escaped]::before {
visibility: visible;
content: attr(data-escaped);