Merge pull request 'i18n: UX improvements: Team permissions and issue closing' (#5383) from fnetx/team-permissions-i18n into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/5383
Reviewed-by: 0ko <0ko@noreply.codeberg.org>
This commit is contained in:
Otto 2024-09-24 21:16:40 +00:00
commit 8b62d3d5da
4 changed files with 59 additions and 15 deletions

View file

@ -245,6 +245,7 @@ func (u *Type) CanBeDefault() bool {
// Unit is a section of one repository // Unit is a section of one repository
type Unit struct { type Unit struct {
Type Type Type Type
Name string
NameKey string NameKey string
URI string URI string
DescKey string DescKey string
@ -272,6 +273,7 @@ func (u Unit) MaxPerm() perm.AccessMode {
var ( var (
UnitCode = Unit{ UnitCode = Unit{
TypeCode, TypeCode,
"code",
"repo.code", "repo.code",
"/", "/",
"repo.code.desc", "repo.code.desc",
@ -281,6 +283,7 @@ var (
UnitIssues = Unit{ UnitIssues = Unit{
TypeIssues, TypeIssues,
"issues",
"repo.issues", "repo.issues",
"/issues", "/issues",
"repo.issues.desc", "repo.issues.desc",
@ -290,6 +293,7 @@ var (
UnitExternalTracker = Unit{ UnitExternalTracker = Unit{
TypeExternalTracker, TypeExternalTracker,
"ext_issues",
"repo.ext_issues", "repo.ext_issues",
"/issues", "/issues",
"repo.ext_issues.desc", "repo.ext_issues.desc",
@ -299,6 +303,7 @@ var (
UnitPullRequests = Unit{ UnitPullRequests = Unit{
TypePullRequests, TypePullRequests,
"pulls",
"repo.pulls", "repo.pulls",
"/pulls", "/pulls",
"repo.pulls.desc", "repo.pulls.desc",
@ -308,6 +313,7 @@ var (
UnitReleases = Unit{ UnitReleases = Unit{
TypeReleases, TypeReleases,
"releases",
"repo.releases", "repo.releases",
"/releases", "/releases",
"repo.releases.desc", "repo.releases.desc",
@ -317,6 +323,7 @@ var (
UnitWiki = Unit{ UnitWiki = Unit{
TypeWiki, TypeWiki,
"wiki",
"repo.wiki", "repo.wiki",
"/wiki", "/wiki",
"repo.wiki.desc", "repo.wiki.desc",
@ -326,6 +333,7 @@ var (
UnitExternalWiki = Unit{ UnitExternalWiki = Unit{
TypeExternalWiki, TypeExternalWiki,
"ext_wiki",
"repo.ext_wiki", "repo.ext_wiki",
"/wiki", "/wiki",
"repo.ext_wiki.desc", "repo.ext_wiki.desc",
@ -335,6 +343,7 @@ var (
UnitProjects = Unit{ UnitProjects = Unit{
TypeProjects, TypeProjects,
"projects",
"repo.projects", "repo.projects",
"/projects", "/projects",
"repo.projects.desc", "repo.projects.desc",
@ -344,6 +353,7 @@ var (
UnitPackages = Unit{ UnitPackages = Unit{
TypePackages, TypePackages,
"packages",
"repo.packages", "repo.packages",
"/packages", "/packages",
"packages.desc", "packages.desc",
@ -353,6 +363,7 @@ var (
UnitActions = Unit{ UnitActions = Unit{
TypeActions, TypeActions,
"actions",
"repo.actions", "repo.actions",
"/actions", "/actions",
"actions.unit.desc", "actions.unit.desc",

View file

@ -1441,8 +1441,7 @@ commitstatus.failure = Failure
commitstatus.pending = Pending commitstatus.pending = Pending
commitstatus.success = Success commitstatus.success = Success
ext_issues = Access to external issues ext_issues = External issues
ext_issues.desc = Link to an external issue tracker.
projects = Projects projects = Projects
projects.desc = Manage issues and pulls in project boards. projects.desc = Manage issues and pulls in project boards.
@ -1619,9 +1618,9 @@ issues.no_content = No description provided.
issues.close = Close issue issues.close = Close issue
issues.comment_pull_merged_at = merged commit %[1]s into %[2]s %[3]s issues.comment_pull_merged_at = merged commit %[1]s into %[2]s %[3]s
issues.comment_manually_pull_merged_at = manually merged commit %[1]s into %[2]s %[3]s issues.comment_manually_pull_merged_at = manually merged commit %[1]s into %[2]s %[3]s
issues.close_comment_issue = Comment and close issues.close_comment_issue = Close with comment
issues.reopen_issue = Reopen issues.reopen_issue = Reopen
issues.reopen_comment_issue = Comment and reopen issues.reopen_comment_issue = Reopen with comment
issues.create_comment = Comment issues.create_comment = Comment
issues.closed_at = `closed this issue <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.closed_at = `closed this issue <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.reopened_at = `reopened this issue <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.reopened_at = `reopened this issue <a id="%[1]s" href="#%[1]s">%[2]s</a>`
@ -2025,8 +2024,7 @@ signing.wont_sign.commitssigned = The merge will not be signed as all the associ
signing.wont_sign.approved = The merge will not be signed as the PR is not approved. signing.wont_sign.approved = The merge will not be signed as the PR is not approved.
signing.wont_sign.not_signed_in = You are not signed in. signing.wont_sign.not_signed_in = You are not signed in.
ext_wiki = Access to external Wiki ext_wiki = External Wiki
ext_wiki.desc = Link to an external wiki.
wiki = Wiki wiki = Wiki
wiki.welcome = Welcome to the Wiki. wiki.welcome = Welcome to the Wiki.
@ -2766,6 +2764,26 @@ error.csv.unexpected = Can't render this file because it contains an unexpected
error.csv.invalid_field_count = Can't render this file because it has a wrong number of fields in line %d. error.csv.invalid_field_count = Can't render this file because it has a wrong number of fields in line %d.
error.broken_git_hook = Git hooks of this repository seem to be broken. Please follow the <a target="_blank" rel="noreferrer" href="%s">documentation</a> to fix them, then push some commits to refresh the status. error.broken_git_hook = Git hooks of this repository seem to be broken. Please follow the <a target="_blank" rel="noreferrer" href="%s">documentation</a> to fix them, then push some commits to refresh the status.
[repo.permissions]
code.read = <b>Read:</b> Access and clone the code of the repository.
code.write = <b>Write:</b> Push to the repository, create branches and tags.
issues.read = <b>Read:</b> Read and create issues and comments.
issues.write = <b>Write:</b> Close issues and manage metadata like labels, milestones, assignees, due dates and dependencies.
pulls.read = <b>Read:</b> Reading and create pull requests.
pulls.write = <b>Write:</b> Close pull requests and manage metadata like labels, milestones, assignees, due dates and dependencies.
releases.read = <b>Read:</b> View and download releases.
releases.write = <b>Write:</b> Publish, edit and delete releases and their assets.
wiki.read = <b>Read:</b> Read the integrated wiki and it's history.
wiki.write = <b>Write:</b> Create, update and delete pages in the integrated wiki.
projects.read = <b>Read:</b> Access repository project boards.
projects.write = <b>Write:</b> Create projects and columns and edit them.
packages.read = <b>Read:</b> View and download packages assigned to the repository.
packages.write = <b>Write:</b> Publish and delete packages assigned to the repository.
actions.read = <b>Read:</b> View integrated CI/CD pipelines and their logs.
actions.write = <b>Write:</b> Manually trigger, restart, cancel or approve pending CI/CD pipelines.
ext_issues = Access the link to an external issue tracker. The permissions are managed externally.
ext_wiki = Access the link to an external wiki. The permissions are managed externally.
[graphs] [graphs]
component_loading = Loading %s... component_loading = Loading %s...
component_loading_failed = Could not load %s component_loading_failed = Could not load %s
@ -3733,7 +3751,7 @@ management = Manage secrets
[actions] [actions]
actions = Actions actions = Actions
unit.desc = Manage integrated CI/CD pipelines with Forgejo Actions unit.desc = Manage integrated CI/CD pipelines with Forgejo Actions.
status.unknown = Unknown status.unknown = Unknown
status.waiting = Waiting status.waiting = Waiting

View file

@ -65,8 +65,8 @@
<tr> <tr>
<th>{{ctx.Locale.Tr "units.unit"}}</th> <th>{{ctx.Locale.Tr "units.unit"}}</th>
<th id="access_none">{{ctx.Locale.Tr "org.teams.none_access"}}</th> <th id="access_none">{{ctx.Locale.Tr "org.teams.none_access"}}</th>
<th id="access_read">{{ctx.Locale.Tr "org.teams.read_access"}}</th> <th>{{ctx.Locale.Tr "org.teams.read_access"}}</th>
<th id="access_write">{{ctx.Locale.Tr "org.teams.write_access"}}</th> <th>{{ctx.Locale.Tr "org.teams.write_access"}}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -75,25 +75,26 @@
<tr> <tr>
<td> <td>
<label {{if $unit.Type.UnitGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}> <label {{if $unit.Type.UnitGlobalDisabled}} data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
{{ctx.Locale.Tr $unit.NameKey}}{{if $unit.Type.UnitGlobalDisabled}} {{ctx.Locale.Tr "org.team_unit_disabled"}}{{end}} <span id="help_{{$unit.Type.Value}}_name">{{ctx.Locale.Tr $unit.NameKey}}{{if $unit.Type.UnitGlobalDisabled}} {{ctx.Locale.Tr "org.team_unit_disabled"}}{{end}}</span>
<span class="help">{{ctx.Locale.Tr $unit.DescKey}}</span> <span class="help" id="help_{{$unit.Type.Value}}_r">{{ctx.Locale.Tr (print "repo.permissions." $unit.Name ".read")}}</span>
<span class="help" id="help_{{$unit.Type.Value}}_w">{{ctx.Locale.Tr (print "repo.permissions." $unit.Name ".write")}}</span>
</label> </label>
</td> </td>
<td> <td>
<label> <label>
<input aria-labelledby="access_none" type="radio" name="unit_{{$unit.Type.Value}}" value="0"{{if or ($unit.Type.UnitGlobalDisabled) (eq ($.Team.UnitAccessMode $.Context $unit.Type) 0)}} checked{{end}}> <input aria-labelledby="help_{{$unit.Type.Value}}_name access_none" type="radio" name="unit_{{$unit.Type.Value}}" value="0"{{if or ($unit.Type.UnitGlobalDisabled) (eq ($.Team.UnitAccessMode $.Context $unit.Type) 0)}} checked{{end}}>
<span class="only-mobile">{{ctx.Locale.Tr "org.teams.none_access"}}</span> <span class="only-mobile">{{ctx.Locale.Tr "org.teams.none_access"}}</span>
</label> </label>
</td> </td>
<td> <td>
<label> <label>
<input aria-labelledby="access_read" type="radio" name="unit_{{$unit.Type.Value}}" value="1"{{if or (eq $.Team.ID 0) (eq ($.Team.UnitAccessMode $.Context $unit.Type) 1)}} checked{{end}} {{if $unit.Type.UnitGlobalDisabled}}disabled{{end}}> <input aria-labelledby="help_{{$unit.Type.Value}}_name help_{{$unit.Type.Value}}_r" type="radio" name="unit_{{$unit.Type.Value}}" value="1"{{if or (eq $.Team.ID 0) (eq ($.Team.UnitAccessMode $.Context $unit.Type) 1)}} checked{{end}} {{if $unit.Type.UnitGlobalDisabled}}disabled{{end}}>
<span class="only-mobile">{{ctx.Locale.Tr "org.teams.read_access"}}</span> <span class="only-mobile">{{ctx.Locale.Tr "org.teams.read_access"}}</span>
</label> </label>
</td> </td>
<td> <td>
<label> <label>
<input aria-labelledby="access_write" type="radio" name="unit_{{$unit.Type.Value}}" value="2"{{if (ge ($.Team.UnitAccessMode $.Context $unit.Type) 2)}} checked{{end}} {{if $unit.Type.UnitGlobalDisabled}}disabled{{end}}> <input aria-labelledby="help_{{$unit.Type.Value}}_name help_{{$unit.Type.Value}}_w" type="radio" name="unit_{{$unit.Type.Value}}" value="2"{{if (ge ($.Team.UnitAccessMode $.Context $unit.Type) 2)}} checked{{end}} {{if $unit.Type.UnitGlobalDisabled}}disabled{{end}}>
<span class="only-mobile">{{ctx.Locale.Tr "org.teams.write_access"}}</span> <span class="only-mobile">{{ctx.Locale.Tr "org.teams.write_access"}}</span>
</label> </label>
</td> </td>
@ -108,7 +109,7 @@
<label {{if $unit.Type.UnitGlobalDisabled}}data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}> <label {{if $unit.Type.UnitGlobalDisabled}}data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{end}}>
<input type="checkbox" name="unit_{{$unit.Type.Value}}" value="1"{{if or (eq $.Team.ID 0) (eq ($.Team.UnitAccessMode $.Context $unit.Type) 1)}} checked{{end}} {{if $unit.Type.UnitGlobalDisabled}}disabled{{end}}> <input type="checkbox" name="unit_{{$unit.Type.Value}}" value="1"{{if or (eq $.Team.ID 0) (eq ($.Team.UnitAccessMode $.Context $unit.Type) 1)}} checked{{end}} {{if $unit.Type.UnitGlobalDisabled}}disabled{{end}}>
{{ctx.Locale.Tr $unit.NameKey}}{{if $unit.Type.UnitGlobalDisabled}} {{ctx.Locale.Tr "org.team_unit_disabled"}}{{end}} {{ctx.Locale.Tr $unit.NameKey}}{{if $unit.Type.UnitGlobalDisabled}} {{ctx.Locale.Tr "org.team_unit_disabled"}}{{end}}
<span class="help">{{ctx.Locale.Tr $unit.DescKey}}</span> <span class="help">{{ctx.Locale.Tr (print "repo.permissions." $unit.Name)}}</span>
</label> </label>
{{end}} {{end}}
{{end}} {{end}}

View file

@ -21,10 +21,24 @@ export async function validate_form({page}, scope) {
await expect(b).toHaveCSS('margin-top', '0px'); await expect(b).toHaveCSS('margin-top', '0px');
await expect(b).toHaveCSS('vertical-align', 'baseline'); await expect(b).toHaveCSS('vertical-align', 'baseline');
} }
// assert no (trailing) colon is used in labels // assert no (trailing) colon is used in labels
// might be necessary to adjust in case colons are strictly necessary in help text // might be necessary to adjust in case colons are strictly necessary in help text
for (const l of await page.locator('label').all()) { for (const l of await page.locator('label').all()) {
const str = await l.textContent(); const str = await l.textContent();
await expect(str.split('\n')[0]).not.toContain(':'); await expect(str.split('\n')[0]).not.toContain(':');
} }
// check that multiple help text are correctly aligned to each other
// used for example to separate read/write permissions in team permission matrix
for (const l of await page.locator('label:has(.help + .help)').all()) {
const helpLabels = await l.locator('.help').all();
const boxes = await Promise.all(helpLabels.map((help) => help.boundingBox()));
for (let i = 1; i < boxes.length; i++) {
// help texts vertically aligned on top of each other
await expect(boxes[i].x).toBe(boxes[0].x);
// help texts don't horizontally intersect each other
await expect(boxes[i].y + boxes[i].height).toBeGreaterThanOrEqual(boxes[i - 1].y + boxes[i - 1].height);
}
}
} }