Compare commits

...

3 Commits

Author SHA1 Message Date
Giteabot
6c5f0af45d
feat: return time of last usage for public keys and access tokens in the api (#34323) (#34339)
Backport #34323 by @tobiasbp

In the Gitea GUI, the user can see the time that _AccessTokens_ and
_PublicKeys_ were last used. This information is not returned by the
_/users/{username}/tokens_ and _/user/keys_ endpoints in the API. This
PR adds the missing data.

The time of last usage for for _tokens_ & _keys_ seem to be stored in
the _Updated_ field of the structs internally. For consistency, I have
used the name _updated_at_ for the new field returned by the _API_.
However, for the _API_ user, I don't think that name reflects the data
returned, as I believe it is the time of last usage. I propose that we
use the name _last_used_at_ instead. Let's hear reviewers opinion on
that.

* PublicKey
  1. _last_used_at_: string($date-time)
* AccessToken
  1. _created_at_: string($date-time) (for parity with public keys)
  2. _last_used_at_: string($date-time)

Fix #34313

Co-authored-by: Tobias Balle-Petersen <tobiasbp@gmail.com>
2025-05-01 14:45:08 -07:00
Giteabot
c95cb7c7e2
fix: do not return archive download URLs in API if downloads are disabled (#34324) (#34338)
Backport #34324 by @tobiasbp

If archive downloads are are disabled using
_DISABLE_DOWNLOAD_SOURCE_ARCHIVES_, archive links are still returned by
the API.

This PR changes the data returned, so the fields _zipball_url_ and
_tarball_url_ are omitted if archive downloads have been disabled.

Resolve #32159

Co-authored-by: Tobias Balle-Petersen <tobiasbp@gmail.com>
2025-05-01 12:18:43 -07:00
Giteabot
6747e3e0eb
Fix some dropdown problems on the issue sidebar (#34308) (#34327)
Backport #34308 by wxiaoguang

Also fix #34300

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-05-01 09:24:18 -07:00
18 changed files with 84 additions and 57 deletions

View File

@ -11,8 +11,8 @@ type Tag struct {
Message string `json:"message"`
ID string `json:"id"`
Commit *CommitMeta `json:"commit"`
ZipballURL string `json:"zipball_url"`
TarballURL string `json:"tarball_url"`
ZipballURL string `json:"zipball_url,omitempty"`
TarballURL string `json:"tarball_url,omitempty"`
}
// AnnotatedTag represents an annotated tag

View File

@ -11,11 +11,13 @@ import (
// AccessToken represents an API access token.
// swagger:response AccessToken
type AccessToken struct {
ID int64 `json:"id"`
Name string `json:"name"`
Token string `json:"sha1"`
TokenLastEight string `json:"token_last_eight"`
Scopes []string `json:"scopes"`
ID int64 `json:"id"`
Name string `json:"name"`
Token string `json:"sha1"`
TokenLastEight string `json:"token_last_eight"`
Scopes []string `json:"scopes"`
Created time.Time `json:"created_at"`
Updated time.Time `json:"last_used_at"`
}
// AccessTokenList represents a list of API access token.

View File

@ -16,6 +16,7 @@ type PublicKey struct {
Fingerprint string `json:"fingerprint,omitempty"`
// swagger:strfmt date-time
Created time.Time `json:"created_at,omitempty"`
Updated time.Time `json:"last_used_at,omitempty"`
Owner *User `json:"user,omitempty"`
ReadOnly bool `json:"read_only,omitempty"`
KeyType string `json:"key_type,omitempty"`

View File

@ -62,6 +62,8 @@ func ListAccessTokens(ctx *context.APIContext) {
Name: tokens[i].Name,
TokenLastEight: tokens[i].TokenLastEight,
Scopes: tokens[i].Scope.StringSlice(),
Created: tokens[i].CreatedUnix.AsTime(),
Updated: tokens[i].UpdatedUnix.AsTime(),
}
}

View File

@ -197,13 +197,22 @@ func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch, repo
// ToTag convert a git.Tag to an api.Tag
func ToTag(repo *repo_model.Repository, t *git.Tag) *api.Tag {
tarballURL := util.URLJoin(repo.HTMLURL(), "archive", t.Name+".tar.gz")
zipballURL := util.URLJoin(repo.HTMLURL(), "archive", t.Name+".zip")
// Archive URLs are "" if the download feature is disabled
if setting.Repository.DisableDownloadSourceArchives {
tarballURL = ""
zipballURL = ""
}
return &api.Tag{
Name: t.Name,
Message: strings.TrimSpace(t.Message),
ID: t.ID.String(),
Commit: ToCommitMeta(repo, t),
ZipballURL: util.URLJoin(repo.HTMLURL(), "archive", t.Name+".zip"),
TarballURL: util.URLJoin(repo.HTMLURL(), "archive", t.Name+".tar.gz"),
ZipballURL: zipballURL,
TarballURL: tarballURL,
}
}
@ -307,6 +316,7 @@ func ToPublicKey(apiLink string, key *asymkey_model.PublicKey) *api.PublicKey {
Title: key.Name,
Fingerprint: key.Fingerprint,
Created: key.CreatedUnix.AsTime(),
Updated: key.UpdatedUnix.AsTime(),
}
}

View File

@ -6,8 +6,8 @@
{{if $pageMeta.Issue}}data-update-url="{{$pageMeta.RepoLink}}/issues/assignee?issue_ids={{$pageMeta.Issue.ID}}"{{end}}
>
<input class="combo-value" name="assignee_ids" type="hidden" value="{{$data.SelectedAssigneeIDs}}">
<div class="ui dropdown text-flex-grow {{if not $pageMeta.CanModifyIssueOrPull}}disabled{{end}}">
<a class="text muted">
<div class="ui dropdown full-width {{if not $pageMeta.CanModifyIssueOrPull}}disabled{{end}}">
<a class="fixed-text muted">
<strong>{{ctx.Locale.Tr "repo.issues.new.assignees"}}</strong> {{if $pageMeta.CanModifyIssueOrPull}}{{svg "octicon-gear"}}{{end}}
</a>
<div class="menu">
@ -16,7 +16,7 @@
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_assignees"}}">
</div>
<div class="scrolling menu flex-items-block">
<div class="item clear-selection">{{ctx.Locale.Tr "repo.issues.new.clear_assignees"}}</div>
<div class="item clear-selection" data-text="">{{ctx.Locale.Tr "repo.issues.new.clear_assignees"}}</div>
<div class="divider"></div>
{{range $data.CandidateAssignees}}
<a class="item" href="#" data-value="{{.ID}}">

View File

@ -4,8 +4,8 @@
{{if $pageMeta.Issue}}data-update-url="{{$pageMeta.RepoLink}}/issues/labels?issue_ids={{$pageMeta.Issue.ID}}"{{end}}
>
<input class="combo-value" name="label_ids" type="hidden" value="{{$data.SelectedLabelIDs}}">
<div class="ui dropdown text-flex-grow {{if not $pageMeta.CanModifyIssueOrPull}}disabled{{end}}">
<a class="text muted">
<div class="ui dropdown full-width {{if not $pageMeta.CanModifyIssueOrPull}}disabled{{end}}">
<a class="fixed-text muted">
<strong>{{ctx.Locale.Tr "repo.issues.new.labels"}}</strong> {{if $pageMeta.CanModifyIssueOrPull}}{{svg "octicon-gear"}}{{end}}
</a>
<div class="menu">
@ -17,7 +17,7 @@
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_labels"}}">
</div>
<div class="scrolling menu">
<a class="item clear-selection" href="#">{{ctx.Locale.Tr "repo.issues.new.clear_labels"}}</a>
<a class="item clear-selection" data-text="" href="#">{{ctx.Locale.Tr "repo.issues.new.clear_labels"}}</a>
<div class="divider"></div>
{{$previousExclusiveScope := "_no_scope"}}
{{range $data.RepoLabels}}

View File

@ -6,8 +6,8 @@
{{if $pageMeta.Issue}}data-update-url="{{$pageMeta.RepoLink}}/issues/milestone?issue_ids={{$pageMeta.Issue.ID}}"{{end}}
>
<input class="combo-value" name="milestone_id" type="hidden" value="{{$data.SelectedMilestoneID}}">
<div class="ui dropdown text-flex-grow {{if not $pageMeta.CanModifyIssueOrPull}}disabled{{end}}">
<a class="text muted">
<div class="ui dropdown full-width {{if not $pageMeta.CanModifyIssueOrPull}}disabled{{end}}">
<a class="fixed-text muted">
<strong>{{ctx.Locale.Tr "repo.issues.new.milestone"}}</strong> {{if $pageMeta.CanModifyIssueOrPull}}{{svg "octicon-gear"}}{{end}}
</a>
<div class="menu">
@ -19,7 +19,7 @@
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_milestones"}}">
</div>
<div class="scrolling menu">
<div class="item clear-selection">{{ctx.Locale.Tr "repo.issues.new.clear_milestone"}}</div>
<div class="item clear-selection" data-text="">{{ctx.Locale.Tr "repo.issues.new.clear_milestone"}}</div>
<div class="divider"></div>
{{if $data.OpenMilestones}}
<div class="header">{{ctx.Locale.Tr "repo.issues.filter_milestone_open"}}</div>

View File

@ -6,8 +6,8 @@
{{if $pageMeta.Issue}}data-update-url="{{$pageMeta.RepoLink}}/issues/projects?issue_ids={{$pageMeta.Issue.ID}}"{{end}}
>
<input class="combo-value" name="project_id" type="hidden" value="{{$data.SelectedProjectID}}">
<div class="ui dropdown text-flex-grow {{if not $pageMeta.CanModifyIssueOrPull}}disabled{{end}}">
<a class="text muted">
<div class="ui dropdown full-width {{if not $pageMeta.CanModifyIssueOrPull}}disabled{{end}}">
<a class="fixed-text muted">
<strong>{{ctx.Locale.Tr "repo.issues.new.projects"}}</strong> {{if $pageMeta.CanModifyIssueOrPull}}{{svg "octicon-gear"}}{{end}}
</a>
<div class="menu">
@ -18,7 +18,7 @@
</div>
{{end}}
<div class="scrolling menu">
<div class="item clear-selection">{{ctx.Locale.Tr "repo.issues.new.clear_projects"}}</div>
<div class="item clear-selection" data-text="">{{ctx.Locale.Tr "repo.issues.new.clear_projects"}}</div>
<div class="divider"></div>
{{if $data.OpenProjects}}
<div class="header">{{ctx.Locale.Tr "repo.issues.new.open_projects"}}</div>

View File

@ -6,8 +6,8 @@
{{if $pageMeta.Issue}}data-update-url="{{$pageMeta.RepoLink}}/issues/request_review?issue_ids={{$pageMeta.Issue.ID}}"{{end}}
>
<input type="hidden" class="combo-value" name="reviewer_ids">{{/* match CreateIssueForm */}}
<div class="ui dropdown text-flex-grow {{if or (not $hasCandidates) (not $data.CanChooseReviewer)}}disabled{{end}}">
<a class="text muted">
<div class="ui dropdown full-width {{if or (not $hasCandidates) (not $data.CanChooseReviewer)}}disabled{{end}}">
<a class="fixed-text muted">
<strong>{{ctx.Locale.Tr "repo.issues.review.reviewers"}}</strong> {{if $data.CanChooseReviewer}}{{svg "octicon-gear"}}{{end}}
</a>
<div class="menu flex-items-menu">

View File

@ -2,10 +2,11 @@
{{if and .CanUseTimetracker (not .Repository.IsArchived)}}
<div class="divider"></div>
<div>
<div class="ui dropdown text-flex-grow jump">
<a class="text muted">
<div class="ui dropdown full-width jump">
<a class="fixed-text muted">
<div>
<strong>{{ctx.Locale.Tr "repo.issues.tracker"}}</strong> {{if $.IsStopwatchRunning}}{{svg "octicon-stopwatch"}}{{end}}
<strong>{{ctx.Locale.Tr "repo.issues.tracker"}}</strong>
{{if $.IsStopwatchRunning}}{{svg "octicon-stopwatch"}}{{end}}
</div>
{{svg "octicon-gear"}}
</a>

View File

@ -19978,11 +19978,21 @@
"type": "object",
"title": "AccessToken represents an API access token.",
"properties": {
"created_at": {
"type": "string",
"format": "date-time",
"x-go-name": "Created"
},
"id": {
"type": "integer",
"format": "int64",
"x-go-name": "ID"
},
"last_used_at": {
"type": "string",
"format": "date-time",
"x-go-name": "Updated"
},
"name": {
"type": "string",
"x-go-name": "Name"
@ -25477,6 +25487,11 @@
"type": "string",
"x-go-name": "KeyType"
},
"last_used_at": {
"type": "string",
"format": "date-time",
"x-go-name": "Updated"
},
"read_only": {
"type": "boolean",
"x-go-name": "ReadOnly"

View File

@ -1188,16 +1188,6 @@ the "!important" is necessary to override Fomantic UI menu item styles, meanwhil
text-overflow: ellipsis !important;
}
.ui.dropdown.text-flex-grow {
display: flex;
}
.ui.dropdown.text-flex-grow > .text {
display: flex;
flex-grow: 1;
justify-content: space-between;
}
.svg.octicon-file-directory-fill,
.svg.octicon-file-directory-open-fill,
.svg.octicon-file-submodule {

View File

@ -50,23 +50,33 @@
width: 300px;
}
.issue-sidebar-combo .ui.dropdown .item:not(.checked) .item-check-mark {
visibility: hidden;
.issue-content-right .ui.dropdown.full-width {
width: 100%;
}
.issue-content-right .dropdown > .menu {
.issue-content-right .ui.dropdown.full-width > .fixed-text {
display: flex;
flex-grow: 1;
justify-content: space-between;
}
.issue-content-right .ui.dropdown > .menu {
max-width: 270px;
min-width: 0;
max-height: 500px;
overflow-x: auto;
}
.issue-content-right .dropdown > .menu .item-secondary-info small {
.issue-content-right .ui.dropdown > .menu .item-secondary-info small {
display: block;
text-overflow: ellipsis;
overflow: hidden;
}
.issue-sidebar-combo > .ui.dropdown .item:not(.checked) .item-check-mark {
visibility: hidden;
}
@media (max-width: 767.98px) {
.issue-content-left,
.issue-content-right {

View File

@ -4080,7 +4080,7 @@ $.fn.dropdown.settings = {
search : 'input.search, .menu > .search > input, .menu input.search',
sizer : '> span.sizer',
text : '> .text:not(.icon)',
unselectable : '.disabled, .filtered',
unselectable : '.disabled, .filtered, .tw-hidden', // GITEA-PATCH: tw-hidden hides the item so it is also unselectable
clearIcon : '> .remove.icon'
},

View File

@ -1,6 +1,6 @@
import {fomanticQuery} from '../modules/fomantic/base.ts';
import {POST} from '../modules/fetch.ts';
import {queryElemChildren, queryElems, toggleElem} from '../utils/dom.ts';
import {addDelegatedEventListener, queryElemChildren, queryElems, toggleElem} from '../utils/dom.ts';
// if there are draft comments, confirm before reloading, to avoid losing comments
function issueSidebarReloadConfirmDraftComment() {
@ -22,7 +22,7 @@ function issueSidebarReloadConfirmDraftComment() {
window.location.reload();
}
class IssueSidebarComboList {
export class IssueSidebarComboList {
updateUrl: string;
updateAlgo: string;
selectionMode: string;
@ -95,9 +95,7 @@ class IssueSidebarComboList {
}
}
async onItemClick(e: Event) {
const elItem = (e.target as HTMLElement).closest('.item');
if (!elItem) return;
async onItemClick(elItem: HTMLElement, e: Event) {
e.preventDefault();
if (elItem.hasAttribute('data-can-change') && elItem.getAttribute('data-can-change') !== 'true') return;
@ -146,16 +144,13 @@ class IssueSidebarComboList {
}
this.initialValues = this.collectCheckedValues();
this.elDropdown.addEventListener('click', (e) => this.onItemClick(e));
addDelegatedEventListener(this.elDropdown, 'click', '.item', (el, e) => this.onItemClick(el, e));
fomanticQuery(this.elDropdown).dropdown('setting', {
action: 'nothing', // do not hide the menu if user presses Enter
fullTextSearch: 'exact',
hideDividers: 'empty',
onHide: () => this.onHide(),
});
}
}
export function initIssueSidebarComboList(container: HTMLElement) {
new IssueSidebarComboList(container).init();
}

View File

@ -1,6 +1,6 @@
import {POST} from '../modules/fetch.ts';
import {queryElems, toggleElem} from '../utils/dom.ts';
import {initIssueSidebarComboList} from './repo-issue-sidebar-combolist.ts';
import {IssueSidebarComboList} from './repo-issue-sidebar-combolist.ts';
function initBranchSelector() {
// TODO: RemoveIssueRef: see "repo/issue/branch_selector_field.tmpl"
@ -48,5 +48,5 @@ export function initRepoIssueSidebar() {
initRepoIssueDue();
// init the combo list: a dropdown for selecting items, and a list for showing selected items and related actions
queryElems<HTMLElement>(document, '.issue-sidebar-combo', (el) => initIssueSidebarComboList(el));
queryElems<HTMLElement>(document, '.issue-sidebar-combo', (el) => new IssueSidebarComboList(el).init());
}

View File

@ -228,12 +228,13 @@ function attachDomEvents(dropdown: HTMLElement, focusable: HTMLElement, menu: HT
dropdown.addEventListener('keydown', (e: KeyboardEvent) => {
// here it must use keydown event before dropdown's keyup handler, otherwise there is no Enter event in our keyup handler
if (e.key === 'Enter') {
const dropdownCall = fomanticDropdownFn.bind($(dropdown));
let $item = dropdownCall('get item', dropdownCall('get value'));
if (!$item) $item = $(menu).find('> .item.selected'); // when dropdown filters items by input, there is no "value", so query the "selected" item
const elItem = menu.querySelector<HTMLElement>(':scope > .item.selected, .menu > .item.selected');
// if the selected item is clickable, then trigger the click event.
// we can not click any item without check, because Fomantic code might also handle the Enter event. that would result in double click.
if ($item?.[0]?.matches('a, .js-aria-clickable')) $item[0].click();
if (elItem?.matches('a, .js-aria-clickable') && !elItem.matches('.tw-hidden, .filtered')) {
e.preventDefault();
elItem.click();
}
}
});