Improve "generate new access token" form (#33730)

Fix: https://github.com/go-gitea/gitea/issues/33519

As discussed in [PR
#33614](https://github.com/go-gitea/gitea/pull/33614), the
ScopedAccessTokenSelector Vue component is not particularly useful.

This PR removes the component and reverts to using HTML templates. It
also introduces some (hopefully) useful refactoring.

The Vue component was causing the UX bug reported in the linked issue.
Required form fields are now properly working, as expected (see
screenshot).

![Screenshot from 2025-02-25
22-00-28](https://github.com/user-attachments/assets/41167854-0718-48b0-a3ee-75ca3a7b8b20)

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
pull/33502/merge
Guillaume 17 hours ago committed by GitHub
parent 8362a41559
commit 303af554c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -5,6 +5,7 @@ package auth
import (
"fmt"
"slices"
"strings"
"code.gitea.io/gitea/models/perm"
@ -14,7 +15,7 @@ import (
type AccessTokenScopeCategory int
const (
AccessTokenScopeCategoryActivityPub = iota
AccessTokenScopeCategoryActivityPub AccessTokenScopeCategory = iota
AccessTokenScopeCategoryAdmin
AccessTokenScopeCategoryMisc // WARN: this is now just a placeholder, don't remove it which will change the following values
AccessTokenScopeCategoryNotification
@ -193,6 +194,14 @@ var accessTokenScopes = map[AccessTokenScopeLevel]map[AccessTokenScopeCategory]A
},
}
func GetAccessTokenCategories() (res []string) {
for _, cat := range accessTokenScopes[Read] {
res = append(res, strings.TrimPrefix(string(cat), "read:"))
}
slices.Sort(res)
return res
}
// GetRequiredScopes gets the specific scopes for a given level and categories
func GetRequiredScopes(level AccessTokenScopeLevel, scopeCategories ...AccessTokenScopeCategory) []AccessTokenScope {
scopes := make([]AccessTokenScope, 0, len(scopeCategories))
@ -270,6 +279,9 @@ func (s AccessTokenScope) parse() (accessTokenScopeBitmap, error) {
// StringSlice returns the AccessTokenScope as a []string
func (s AccessTokenScope) StringSlice() []string {
if s == "" {
return nil
}
return strings.Split(string(s), ",")
}

@ -17,6 +17,7 @@ type scopeTestNormalize struct {
}
func TestAccessTokenScope_Normalize(t *testing.T) {
assert.Equal(t, []string{"activitypub", "admin", "issue", "misc", "notification", "organization", "package", "repository", "user"}, GetAccessTokenCategories())
tests := []scopeTestNormalize{
{"", "", nil},
{"write:misc,write:notification,read:package,write:notification,public-only", "public-only,write:misc,write:notification,read:package", nil},
@ -25,7 +26,7 @@ func TestAccessTokenScope_Normalize(t *testing.T) {
{"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user,public-only", "public-only,all", nil},
}
for _, scope := range []string{"activitypub", "admin", "misc", "notification", "organization", "package", "issue", "repository", "user"} {
for _, scope := range GetAccessTokenCategories() {
tests = append(tests,
scopeTestNormalize{AccessTokenScope(fmt.Sprintf("read:%s", scope)), AccessTokenScope(fmt.Sprintf("read:%s", scope)), nil},
scopeTestNormalize{AccessTokenScope(fmt.Sprintf("write:%s", scope)), AccessTokenScope(fmt.Sprintf("write:%s", scope)), nil},
@ -59,7 +60,7 @@ func TestAccessTokenScope_HasScope(t *testing.T) {
{"public-only", "read:issue", false, nil},
}
for _, scope := range []string{"activitypub", "admin", "misc", "notification", "organization", "package", "issue", "repository", "user"} {
for _, scope := range GetAccessTokenCategories() {
tests = append(tests,
scopeTestHasScope{
AccessTokenScope(fmt.Sprintf("read:%s", scope)),

@ -917,7 +917,6 @@ delete_token_success = The token has been deleted. Applications using it no long
repo_and_org_access = Repository and Organization Access
permissions_public_only = Public only
permissions_access_all = All (public, private, and limited)
select_permissions = Select permissions
permission_not_set = Not set
permission_no_access = No Access
permission_read = Read

@ -6,12 +6,14 @@ package setting
import (
"net/http"
"strings"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
@ -39,18 +41,29 @@ func ApplicationsPost(ctx *context.Context) {
ctx.Data["PageIsSettingsApplications"] = true
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
if ctx.HasError() {
loadApplicationsData(ctx)
ctx.HTML(http.StatusOK, tplSettingsApplications)
return
_ = ctx.Req.ParseForm()
var scopeNames []string
for k, v := range ctx.Req.Form {
if strings.HasPrefix(k, "scope-") {
scopeNames = append(scopeNames, v...)
}
}
scope, err := form.GetScope()
scope, err := auth_model.AccessTokenScope(strings.Join(scopeNames, ",")).Normalize()
if err != nil {
ctx.ServerError("GetScope", err)
return
}
if scope == "" || scope == auth_model.AccessTokenScopePublicOnly {
ctx.Flash.Error(ctx.Tr("settings.at_least_one_permission"), true)
}
if ctx.HasError() {
loadApplicationsData(ctx)
ctx.HTML(http.StatusOK, tplSettingsApplications)
return
}
t := &auth_model.AccessToken{
UID: ctx.Doer.ID,
Name: form.Name,
@ -99,7 +112,14 @@ func loadApplicationsData(ctx *context.Context) {
}
ctx.Data["Tokens"] = tokens
ctx.Data["EnableOAuth2"] = setting.OAuth2.Enabled
ctx.Data["IsAdmin"] = ctx.Doer.IsAdmin
// Handle specific ordered token categories for admin or non-admin users
tokenCategoryNames := auth_model.GetAccessTokenCategories()
if !ctx.Doer.IsAdmin {
tokenCategoryNames = util.SliceRemoveAll(tokenCategoryNames, "admin")
}
ctx.Data["TokenCategories"] = tokenCategoryNames
if setting.OAuth2.Enabled {
ctx.Data["Applications"], err = db.Find[auth_model.OAuth2Application](ctx, auth_model.FindOAuth2ApplicationsOptions{
OwnerID: ctx.Doer.ID,

@ -213,13 +213,16 @@ func Contexter() func(next http.Handler) http.Handler {
// Attention: this function changes ctx.Data and ctx.Flash
// If HasError is called, then before Redirect, the error message should be stored by ctx.Flash.Error(ctx.GetErrMsg()) again.
func (ctx *Context) HasError() bool {
hasErr, ok := ctx.Data["HasError"]
if !ok {
hasErr, _ := ctx.Data["HasError"].(bool)
hasErr = hasErr || ctx.Flash.ErrorMsg != ""
if !hasErr {
return false
}
if ctx.Flash.ErrorMsg == "" {
ctx.Flash.ErrorMsg = ctx.GetErrMsg()
}
ctx.Data["Flash"] = ctx.Flash
return hasErr.(bool)
return hasErr
}
// GetErrMsg returns error message in form validation.

@ -7,9 +7,7 @@ package forms
import (
"mime/multipart"
"net/http"
"strings"
auth_model "code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web/middleware"
@ -348,7 +346,6 @@ func (f *EditVariableForm) Validate(req *http.Request, errs binding.Errors) bind
// NewAccessTokenForm form for creating access token
type NewAccessTokenForm struct {
Name string `binding:"Required;MaxSize(255)" locale:"settings.token_name"`
Scope []string
}
// Validate validates the fields
@ -357,12 +354,6 @@ func (f *NewAccessTokenForm) Validate(req *http.Request, errs binding.Errors) bi
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
func (f *NewAccessTokenForm) GetScope() (auth_model.AccessTokenScope, error) {
scope := strings.Join(f.Scope, ",")
s, err := auth_model.AccessTokenScope(scope).Normalize()
return s, err
}
// EditOAuth2ApplicationForm form for editing oauth2 applications
type EditOAuth2ApplicationForm struct {
Name string `binding:"Required;MaxSize(255)" form:"application_name"`

@ -4,10 +4,8 @@
package forms
import (
"strconv"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/modules/setting"
"github.com/gobwas/glob"
@ -104,28 +102,3 @@ func TestRegisterForm_IsDomainAllowed_BlockedEmail(t *testing.T) {
assert.Equal(t, v.valid, form.IsEmailDomainAllowed())
}
}
func TestNewAccessTokenForm_GetScope(t *testing.T) {
tests := []struct {
form NewAccessTokenForm
scope auth_model.AccessTokenScope
expectedErr error
}{
{
form: NewAccessTokenForm{Name: "test", Scope: []string{"read:repository"}},
scope: "read:repository",
},
{
form: NewAccessTokenForm{Name: "test", Scope: []string{"read:repository", "write:user"}},
scope: "read:repository,write:user",
},
}
for i, test := range tests {
t.Run(strconv.Itoa(i), func(t *testing.T) {
scope, err := test.form.GetScope()
assert.Equal(t, test.expectedErr, err)
assert.Equal(t, test.scope, scope)
})
}
}

@ -50,49 +50,41 @@
</div>
</div>
<div class="ui bottom attached segment">
<h5 class="ui top header">
{{ctx.Locale.Tr "settings.generate_new_token"}}
</h5>
<form id="scoped-access-form" class="ui form ignore-dirty" action="{{.Link}}" method="post">
<details {{if or .name (not .Tokens)}}open{{end}}>
<summary><h4 class="ui header tw-inline-block tw-my-2">{{ctx.Locale.Tr "settings.generate_new_token"}}</h4></summary>
<form class="ui form ignore-dirty" action="{{.Link}}" method="post">
{{.CsrfTokenHtml}}
<div class="field {{if .Err_Name}}error{{end}}">
<label for="name">{{ctx.Locale.Tr "settings.token_name"}}</label>
<input id="name" name="name" value="{{.name}}" autofocus required maxlength="255">
<input id="name" name="name" value="{{.name}}" required maxlength="255">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "settings.repo_and_org_access"}}</label>
<label class="tw-cursor-pointer">
<input class="enable-system tw-mt-1 tw-mr-1" type="radio" name="scope" value="{{$.AccessTokenScopePublicOnly}}">
{{ctx.Locale.Tr "settings.permissions_public_only"}}
<div class="tw-my-2">{{ctx.Locale.Tr "settings.repo_and_org_access"}}</div>
<label class="gt-checkbox">
<input type="radio" name="scope-public-only" value="{{$.AccessTokenScopePublicOnly}}"> {{ctx.Locale.Tr "settings.permissions_public_only"}}
</label>
<label class="tw-cursor-pointer">
<input class="enable-system tw-mt-1 tw-mr-1" type="radio" name="scope" value="" checked>
{{ctx.Locale.Tr "settings.permissions_access_all"}}
<label class="gt-checkbox">
<input type="radio" name="scope-public-only" value="" checked> {{ctx.Locale.Tr "settings.permissions_access_all"}}
</label>
</div>
<details class="ui optional field">
<summary class="tw-pb-4 tw-pl-1">
{{ctx.Locale.Tr "settings.select_permissions"}}
</summary>
<p class="activity meta">
<i>{{ctx.Locale.Tr "settings.access_token_desc" (HTMLFormat `href="%s/api/swagger" target="_blank"` AppSubUrl) (`href="https://docs.gitea.com/development/oauth2-provider#scopes" target="_blank"`|SafeHTML)}}</i>
</p>
<div id="scoped-access-token-selector"
data-is-admin="{{if .IsAdmin}}true{{else}}false{{end}}"
data-no-access-label="{{ctx.Locale.Tr "settings.permission_no_access"}}"
data-read-label="{{ctx.Locale.Tr "settings.permission_read"}}"
data-write-label="{{ctx.Locale.Tr "settings.permission_write"}}"
data-locale-component-failed-to-load="{{ctx.Locale.Tr "graphs.component_failed_to_load"}}"
>
<div>
<div class="tw-my-2">{{ctx.Locale.Tr "settings.access_token_desc" (HTMLFormat `href="%s/api/swagger" target="_blank"` AppSubUrl) (`href="https://docs.gitea.com/development/oauth2-provider#scopes" target="_blank"`|SafeHTML)}}</div>
<table class="ui table unstackable tw-my-2">
{{range $category := .TokenCategories}}
<tr>
<td>{{$category}}</td>
<td><label class="gt-checkbox"><input type="radio" name="scope-{{$category}}" value="" checked> {{ctx.Locale.Tr "settings.permission_no_access"}}</label></td>
<td><label class="gt-checkbox"><input type="radio" name="scope-{{$category}}" value="read:{{$category}}"> {{ctx.Locale.Tr "settings.permission_read"}}</label></td>
<td><label class="gt-checkbox"><input type="radio" name="scope-{{$category}}" value="write:{{$category}}"> {{ctx.Locale.Tr "settings.permission_write"}}</label></td>
</tr>
{{end}}
</table>
</div>
</details>
<button id="scoped-access-submit" class="ui primary button">
<button class="ui primary button">
{{ctx.Locale.Tr "settings.generate_token"}}
</button>
</form>{{/* Fomantic ".ui.form .warning.message" is hidden by default, so put the warning message out of the form*/}}
<div id="scoped-access-warning" class="ui warning message center tw-hidden">
{{ctx.Locale.Tr "settings.at_least_one_permission"}}
</div>
</form>
</details>
</div>
{{if .EnableOAuth2}}

@ -48,9 +48,8 @@
</div>
<div class="ui bottom attached segment">
<h5 class="ui top header">
{{ctx.Locale.Tr "settings.create_oauth2_application"}}
</h5>
<details {{if .application_name}}open{{end}}>
<summary><h4 class="ui header tw-inline-block tw-my-2">{{ctx.Locale.Tr "settings.create_oauth2_application"}}</h4></summary>
<form class="ui form ignore-dirty" action="{{.Link}}/oauth2" method="post">
{{.CsrfTokenHtml}}
<div class="field {{if .Err_AppName}}error{{end}}">
@ -77,4 +76,5 @@
{{ctx.Locale.Tr "settings.create_oauth2_application_button"}}
</button>
</form>
</details>
</div>

@ -76,7 +76,7 @@ func TestAPIAdminOrgCreateNotAdmin(t *testing.T) {
defer tests.PrepareTestEnv(t)()
nonAdminUsername := "user2"
session := loginUser(t, nonAdminUsername)
token := getTokenForLoggedInUser(t, session)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll)
org := api.CreateOrgOption{
UserName: "user2_org",
FullName: "User2's organization",

@ -76,7 +76,7 @@ func TestAPIAdminDeleteUnauthorizedKey(t *testing.T) {
var newPublicKey api.PublicKey
DecodeJSON(t, resp, &newPublicKey)
token = getUserToken(t, normalUsername)
token = getUserToken(t, normalUsername, auth_model.AccessTokenScopeAll)
req = NewRequestf(t, "DELETE", "/api/v1/admin/users/%s/keys/%d", adminUsername, newPublicKey.ID).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
@ -139,7 +139,7 @@ func TestAPIListUsersNotLoggedIn(t *testing.T) {
func TestAPIListUsersNonAdmin(t *testing.T) {
defer tests.PrepareTestEnv(t)()
nonAdminUsername := "user2"
token := getUserToken(t, nonAdminUsername)
token := getUserToken(t, nonAdminUsername, auth_model.AccessTokenScopeAll)
req := NewRequest(t, "GET", "/api/v1/admin/users").
AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)

@ -33,6 +33,10 @@ type APITestContext struct {
func NewAPITestContext(t *testing.T, username, reponame string, scope ...auth.AccessTokenScope) APITestContext {
session := loginUser(t, username)
if len(scope) == 0 {
// FIXME: legacy logic: no scope means all
scope = []auth.AccessTokenScope{auth.AccessTokenScopeAll}
}
token := getTokenForLoggedInUser(t, session, scope...)
return APITestContext{
Session: session,

@ -72,7 +72,7 @@ func TestAPIReposGitBlobs(t *testing.T) {
// Login as User4.
session = loginUser(t, user4.Name)
token4 := getTokenForLoggedInUser(t, session)
token4 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll)
// Test using org repo "org3/repo3" where user4 is a NOT collaborator
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/d56a3073c1dbb7b15963110a049d50cdb5db99fc?access=%s", org3.Name, repo3.Name, token4)

@ -69,7 +69,7 @@ func TestAPIReposGitTrees(t *testing.T) {
// Login as User4.
session = loginUser(t, user4.Name)
token4 := getTokenForLoggedInUser(t, session)
token4 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll)
// Test using org repo "org3/repo3" where user4 is a NOT collaborator
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/trees/d56a3073c1dbb7b15963110a049d50cdb5db99fc?access=%s", org3.Name, repo3.Name, token4)

@ -249,55 +249,19 @@ func loginUserWithPassword(t testing.TB, userName, password string) *TestSession
// token has to be unique this counter take care of
var tokenCounter int64
// getTokenForLoggedInUser returns a token for a logged in user.
// The scope is an optional list of snake_case strings like the frontend form fields,
// but without the "scope_" prefix.
// getTokenForLoggedInUser returns a token for a logged-in user.
func getTokenForLoggedInUser(t testing.TB, session *TestSession, scopes ...auth.AccessTokenScope) string {
t.Helper()
var token string
req := NewRequest(t, "GET", "/user/settings/applications")
resp := session.MakeRequest(t, req, http.StatusOK)
var csrf string
for _, cookie := range resp.Result().Cookies() {
if cookie.Name != "_csrf" {
continue
}
csrf = cookie.Value
break
}
if csrf == "" {
doc := NewHTMLParser(t, resp.Body)
csrf = doc.GetCSRF()
}
assert.NotEmpty(t, csrf)
urlValues := url.Values{}
urlValues.Add("_csrf", csrf)
urlValues.Add("_csrf", GetUserCSRFToken(t, session))
urlValues.Add("name", fmt.Sprintf("api-testing-token-%d", atomic.AddInt64(&tokenCounter, 1)))
for _, scope := range scopes {
urlValues.Add("scope", string(scope))
}
req = NewRequestWithURLValues(t, "POST", "/user/settings/applications", urlValues)
resp = session.MakeRequest(t, req, http.StatusSeeOther)
// Log the flash values on failure
if !assert.Equal(t, []string{"/user/settings/applications"}, resp.Result().Header["Location"]) {
for _, cookie := range resp.Result().Cookies() {
if cookie.Name != gitea_context.CookieNameFlash {
continue
urlValues.Add("scope-dummy", string(scope)) // it only needs to start with "scope-" to be accepted
}
flash, _ := url.ParseQuery(cookie.Value)
for key, value := range flash {
t.Logf("Flash %q: %q", key, value)
}
}
}
req = NewRequest(t, "GET", "/user/settings/applications")
resp = session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
token = htmlDoc.doc.Find(".ui.info p").Text()
assert.NotEmpty(t, token)
return token
req := NewRequestWithURLValues(t, "POST", "/user/settings/applications", urlValues)
session.MakeRequest(t, req, http.StatusSeeOther)
flashes := session.GetCookieFlashMessage()
return flashes.InfoMsg
}
type RequestWrapper struct {

@ -19,7 +19,6 @@
@import "./modules/dimmer.css";
@import "./modules/modal.css";
@import "./modules/select.css";
@import "./modules/tippy.css";
@import "./modules/breadcrumb.css";
@import "./modules/comment.css";

@ -119,3 +119,13 @@ input[type="radio"] {
.ui.toggle.checkbox input:focus:checked ~ label::before {
background: var(--color-primary) !important;
}
label.gt-checkbox {
display: inline-flex;
align-items: center;
gap: 0.25em;
}
.ui.form .field > label.gt-checkbox {
display: flex;
}

@ -1,25 +0,0 @@
.gitea-select {
position: relative;
}
.gitea-select select {
appearance: none; /* hide default triangle */
}
/* ::before and ::after pseudo elements don't work on select elements,
so we need to put it on the parent. */
.gitea-select::after {
position: absolute;
top: 12px;
right: 8px;
pointer-events: none;
content: "";
width: 14px;
height: 14px;
mask-size: cover;
-webkit-mask-size: cover;
mask-image: var(--octicon-chevron-right);
-webkit-mask-image: var(--octicon-chevron-right);
transform: rotate(90deg); /* point the chevron down */
background: currentcolor;
}

@ -1,81 +0,0 @@
<script lang="ts" setup>
import {computed, onMounted, onUnmounted} from 'vue';
import {hideElem, showElem} from '../utils/dom.ts';
const props = defineProps<{
isAdmin: boolean;
noAccessLabel: string;
readLabel: string;
writeLabel: string;
}>();
const categories = computed(() => {
const categories = [
'activitypub',
];
if (props.isAdmin) {
categories.push('admin');
}
categories.push(
'issue',
'misc',
'notification',
'organization',
'package',
'repository',
'user');
return categories;
});
onMounted(() => {
document.querySelector('#scoped-access-submit').addEventListener('click', onClickSubmit);
});
onUnmounted(() => {
document.querySelector('#scoped-access-submit').removeEventListener('click', onClickSubmit);
});
function onClickSubmit(e: Event) {
e.preventDefault();
const warningEl = document.querySelector('#scoped-access-warning');
// check that at least one scope has been selected
for (const el of document.querySelectorAll<HTMLInputElement>('.access-token-select')) {
if (el.value) {
// Hide the error if it was visible from previous attempt.
hideElem(warningEl);
// Submit the form.
document.querySelector<HTMLFormElement>('#scoped-access-form').submit();
// Don't show the warning.
return;
}
}
// no scopes selected, show validation error
showElem(warningEl);
}
</script>
<template>
<div v-for="category in categories" :key="category" class="field tw-pl-1 tw-pb-1 access-token-category">
<label class="category-label" :for="'access-token-scope-' + category">
{{ category }}
</label>
<div class="gitea-select">
<select
class="ui selection access-token-select"
name="scope"
:id="'access-token-scope-' + category"
>
<option value="">
{{ noAccessLabel }}
</option>
<option :value="'read:' + category">
{{ readLabel }}
</option>
<option :value="'write:' + category">
{{ writeLabel }}
</option>
</select>
</div>
</div>
</template>

@ -1,20 +0,0 @@
import {createApp} from 'vue';
export async function initScopedAccessTokenCategories() {
const el = document.querySelector('#scoped-access-token-selector');
if (!el) return;
const {default: ScopedAccessTokenSelector} = await import(/* webpackChunkName: "scoped-access-token-selector" */'../components/ScopedAccessTokenSelector.vue');
try {
const View = createApp(ScopedAccessTokenSelector, {
isAdmin: JSON.parse(el.getAttribute('data-is-admin')),
noAccessLabel: el.getAttribute('data-no-access-label'),
readLabel: el.getAttribute('data-read-label'),
writeLabel: el.getAttribute('data-write-label'),
});
View.mount(el);
} catch (err) {
console.error('ScopedAccessTokenSelector failed to load', err);
el.textContent = el.getAttribute('data-locale-component-failed-to-load');
}
}

@ -68,7 +68,6 @@ import {initColorPickers} from './features/colorpicker.ts';
import {initAdminSelfCheck} from './features/admin/selfcheck.ts';
import {initOAuth2SettingsDisableCheckbox} from './features/oauth2-settings.ts';
import {initGlobalFetchAction} from './features/common-fetch-action.ts';
import {initScopedAccessTokenCategories} from './features/scoped-access-token.ts';
import {
initFootLanguageMenu,
initGlobalDropdown,
@ -209,7 +208,6 @@ onDomReady(() => {
initUserSettings,
initRepoDiffView,
initPdfViewer,
initScopedAccessTokenCategories,
initColorPickers,
initOAuth2SettingsDisableCheckbox,

Loading…
Cancel
Save