feat(ui): improve org header with new noJS dropdown and more options (#8572)

Related: https://codeberg.org/forgejo/forgejo/pulls/6977/files#diff-fd05eba523810d46c7763db938ad5839372a074a, https://codeberg.org/forgejo/forgejo/pulls/3949, https://codeberg.org/forgejo/forgejo/pulls/7906

* use the new noJS dropdown for extra actions in org view (currently only includes report button)
    * this required some refactoring of the area because the said dropdown was not built to be placed in an area where `font-size:36px` is forced onto everything
    * this greatly improves consistently with user profiles which now use this type of dropdown
    * I decided against making the opener button mimicrate an actual button because it looks ok as is and is consitent with menu in user profiles and because I don't think this is a good design language to make a kebab menu opener look this way
* add icon to the entry
* add atom entry

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/8572
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Reviewed-by: Beowulf <beowulf@beocode.eu>
Co-authored-by: 0ko <0ko@noreply.codeberg.org>
Co-committed-by: 0ko <0ko@noreply.codeberg.org>
This commit is contained in:
0ko 2025-07-23 02:06:13 +02:00 committed by Gusted
commit c11dd3fb46
5 changed files with 88 additions and 27 deletions

View file

@ -1,4 +1,4 @@
<button class="ui basic button tw-mr-0" hx-post="{{.Org.HomeLink}}?action={{if $.IsFollowing}}unfollow{{else}}follow{{end}}"> <button class="ui basic button" hx-post="{{.Org.HomeLink}}?action={{if $.IsFollowing}}unfollow{{else}}follow{{end}}">
{{if $.IsFollowing}} {{if $.IsFollowing}}
{{ctx.Locale.Tr "user.unfollow"}} {{ctx.Locale.Tr "user.unfollow"}}
{{else}} {{else}}

View file

@ -1,33 +1,41 @@
<div class="ui container tw-flex"> <div class="ui container tw-flex tw-gap-x-4">
{{ctx.AvatarUtils.Avatar .Org 100 "org-avatar"}} {{ctx.AvatarUtils.Avatar .Org 100 "org-avatar"}}
<div id="org-info" class="tw-flex tw-flex-col"> <div id="org-info" class="tw-flex tw-flex-col">
<div class="ui header"> <div class="org-header tw-flex">
<div class="org-title"> <div class="org-title">
{{.Org.DisplayName}} <h1>{{.Org.DisplayName}}</h1>
<span class="org-visibility"> <span class="org-visibility">
{{if .Org.Visibility.IsLimited}}<span class="ui large horizontal label">{{ctx.Locale.Tr "org.settings.visibility.limited_shortname"}}</span>{{end}} {{if .Org.Visibility.IsLimited}}<span class="ui large horizontal label">{{ctx.Locale.Tr "org.settings.visibility.limited_shortname"}}</span>{{end}}
{{if .Org.Visibility.IsPrivate}}<span class="ui large horizontal label">{{ctx.Locale.Tr "org.settings.visibility.private_shortname"}}</span>{{end}} {{if .Org.Visibility.IsPrivate}}<span class="ui large horizontal label">{{ctx.Locale.Tr "org.settings.visibility.private_shortname"}}</span>{{end}}
</span> </span>
</div> </div>
<span class="tw-flex tw-items-center button-row tw-ml-auto tw-text-16 tw-whitespace-nowrap"> <span class="button-sequence tw-items-center tw-ml-auto tw-whitespace-nowrap">
{{if .EnableFeed}}
<a class="ui basic label button tw-mr-0" href="{{.Org.HomeLink}}.rss" data-tooltip-content="{{ctx.Locale.Tr "rss_feed"}}">
{{svg "octicon-rss" 24}}
</a>
{{end}}
{{if .IsSigned}} {{if .IsSigned}}
{{template "org/follow_unfollow" .}} {{template "org/follow_unfollow" .}}
{{end}} {{end}}
{{if .IsOrganizationMember}} {{if .IsOrganizationMember}}
<a class="ui basic button tw-mr-0" href="{{.OrgLink}}/dashboard">{{ctx.Locale.Tr "org.open_dashboard"}}</a> <a class="ui basic button" href="{{.OrgLink}}/dashboard">{{ctx.Locale.Tr "org.open_dashboard"}}</a>
{{end}} {{end}}
{{if and .IsModerationEnabled .IsSigned (not .IsOrganizationOwner)}} {{$moderationEntryNeeded := and .IsModerationEnabled .IsSigned (not .IsOrganizationOwner)}}
<button class="ui dropdown icon button" data-tooltip-content="{{ctx.Locale.Tr "repo.more_operations"}}" aria-label="{{ctx.Locale.Tr "toggle_menu"}}"> {{if or .EnableFeed $moderationEntryNeeded}}
{{svg "octicon-kebab-horizontal" 14}} <details class="dropdown dir-rtl">
<div class="menu top left"> <summary data-tooltip-content="{{ctx.Locale.Tr "profile.actions.tooltip"}}">{{svg "octicon-kebab-horizontal" 20}}</summary>
<a class="item context" href="{{AppSubUrl}}/report_abuse?type=org&id={{$.Org.ID}}">{{ctx.Locale.Tr "moderation.report_abuse"}}</a> <ul>
</div> {{if .EnableFeed}}
</button> <li>
<a class="item" href="{{.Org.HomeLink}}.rss">{{svg "octicon-rss"}}{{ctx.Locale.Tr "rss_feed"}}</a>
</li>
<li>
<a class="item" href="{{.Org.HomeLink}}.atom">{{svg "octicon-rss"}}{{ctx.Locale.Tr "feed.atom.link"}}</a>
</li>
{{end}}
{{if $moderationEntryNeeded}}
<li>
<a class="item orange text" href="{{AppSubUrl}}/report_abuse?type=org&id={{$.Org.ID}}">{{svg "octicon-stop"}}{{ctx.Locale.Tr "moderation.report_abuse"}}</a>
</li>
{{end}}
</ul>
</details>
{{end}} {{end}}
</span> </span>
</div> </div>

View file

@ -104,5 +104,51 @@ quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequa
assert.NotContains(t, resp.Body.String(), "veniam") assert.NotContains(t, resp.Body.String(), "veniam")
}) })
}) })
t.Run("More actions - feeds only", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
defer test.MockVariableValue(&setting.Other.EnableFeed, true)()
defer test.MockVariableValue(&setting.Moderation.Enabled, false)()
// Both guests and logged in users should see the feed option
doc := NewHTMLParser(t, MakeRequest(t, NewRequest(t, "GET", "/org3"), http.StatusOK).Body)
doc.AssertElement(t, "details.dropdown a[href='/org3.rss']", true)
doc.AssertElement(t, "details.dropdown a[href^='/report_abuse']", false)
doc = NewHTMLParser(t, loginUser(t, "user10").MakeRequest(t, NewRequest(t, "GET", "/org3"), http.StatusOK).Body)
doc.AssertElement(t, "details.dropdown a[href='/org3.rss']", true)
doc.AssertElement(t, "details.dropdown a[href^='/report_abuse']", false)
})
t.Run("More actions - none", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
defer test.MockVariableValue(&setting.Other.EnableFeed, false)()
defer test.MockVariableValue(&setting.Moderation.Enabled, false)()
// The dropdown won't appear if no entries are available, for both guests and logged in users
doc := NewHTMLParser(t, MakeRequest(t, NewRequest(t, "GET", "/org3"), http.StatusOK).Body)
doc.AssertElement(t, "details.dropdown", false)
doc = NewHTMLParser(t, loginUser(t, "user10").MakeRequest(t, NewRequest(t, "GET", "/org3"), http.StatusOK).Body)
doc.AssertElement(t, "details.dropdown", false)
})
t.Run("More actions - moderation", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
defer test.MockVariableValue(&setting.Other.EnableFeed, false)()
defer test.MockVariableValue(&setting.Moderation.Enabled, true)()
// The report option shouldn't be available to a guest
doc := NewHTMLParser(t, MakeRequest(t, NewRequest(t, "GET", "/org3"), http.StatusOK).Body)
doc.AssertElement(t, "details.dropdown", false)
// But should be available to a logged in user
doc = NewHTMLParser(t, loginUser(t, "user10").MakeRequest(t, NewRequest(t, "GET", "/org3"), http.StatusOK).Body)
doc.AssertElement(t, "details.dropdown a[href^='/report_abuse']", true)
// But the org owner shouldn't see the report option
doc = NewHTMLParser(t, loginUser(t, "user1").MakeRequest(t, NewRequest(t, "GET", "/org3"), http.StatusOK).Body)
doc.AssertElement(t, "details.dropdown", false)
})
}) })
} }

View file

@ -99,7 +99,13 @@ details.dropdown > summary + ul > li:last-child {
/* Note: https://css-tricks.com/css-anchor-positioning-guide/ /* Note: https://css-tricks.com/css-anchor-positioning-guide/
* looks like a great thing but FF still doesn't support it. */ * looks like a great thing but FF still doesn't support it. */
/* Note: dropdown.dir-rtl can be implemented when needed, e.g. for navbar profile dropdown on desktop layout. */ details.dropdown.dir-rtl > summary + ul {
inset-inline: 0 auto;
direction: rtl;
}
details.dropdown.dir-rtl > summary + ul > li {
direction: ltr;
}
details.dropdown > summary + ul > li > .item { details.dropdown > summary + ul > li > .item {
padding: var(--dropdown-item-padding); padding: var(--dropdown-item-padding);

View file

@ -89,28 +89,29 @@
text-align: center; text-align: center;
} }
.page-content.organization .org-avatar {
margin-right: 15px;
}
.page-content.organization #org-info { .page-content.organization #org-info {
overflow-wrap: anywhere; overflow-wrap: anywhere;
flex: 1; flex: 1;
} }
.page-content.organization #org-info .ui.header { .page-content.organization #org-info .org-title {
display: flex; display: flex;
column-gap: 1rem;
align-items: center; align-items: center;
font-size: 36px; }
margin-bottom: 0;
.page-content.organization #org-info .org-title h1 {
margin: 0;
font-size: 2.5rem;
} }
@media (max-width: 767.98px) { @media (max-width: 767.98px) {
.page-content.organization #org-info .ui.header { .page-content.organization #org-info .org-header {
flex-direction: column; flex-direction: column;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.page-content.organization #org-info .org-title { .page-content.organization #org-info .org-title {
flex-wrap: wrap;
width: 100%; width: 100%;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }