loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Change access token UI to select dropdowns (#25109)

The current UI to create API access tokens uses checkboxes that have a
complicated relationship where some need to be checked and/or disabled
in certain states. It also requires that a user interact with it to
understand what their options really are.

This branch changes to use `<select>`s. It better fits the available
options, and it's closer to [GitHub's
UI](https://github.com/settings/personal-access-tokens/new), which is
good, in my opinion. It's more mobile friendly since the tap-areas are
larger. If we ever add more permissions, like Maintainer, there's a
natural place that doesn't take up more screen real-estate.

This branch also fixes a few minor issues:

- Hide the error about selecting at least one permission after second
submission
- Fix help description to call it "authorization" since that's what
permissions are about (not authentication)

Related: #24767.

<img width="883" alt="Screenshot 2023-06-07 at 5 07 34 PM"
src="https://github.com/go-gitea/gitea/assets/10803/6b63d807-c9be-4a4b-8e53-ecab6cbb8f76">

---

When it's open:

<img width="881" alt="Screenshot 2023-06-07 at 5 07 59 PM"
src="https://github.com/go-gitea/gitea/assets/10803/2432c6d0-39c2-4ca4-820e-c878ffdbfb69">

authored by

Jonathan Tran and committed by
GitHub
a583c563 f62cd2f4

+121 -120
+4 -1
options/locale/locale_en-US.ini
··· 811 811 permissions_public_only = Public only 812 812 permissions_access_all = All (public, private, and limited) 813 813 select_permissions = Select permissions 814 - scoped_token_desc = Selected token scopes limit authentication only to the corresponding <a %s>API</a> routes. Read the <a %s>documentation</a> for more information. 814 + permission_no_access = No Access 815 + permission_read = Read 816 + permission_write = Read and Write 817 + access_token_desc = Selected token permissions limit authorization only to the corresponding <a %s>API</a> routes. Read the <a %s>documentation</a> for more information. 815 818 at_least_one_permission = You must select at least one permission to create a token 816 819 permissions_list = Permissions: 817 820
+10 -13
templates/user/settings/applications.tmpl
··· 69 69 <summary class="gt-pb-4 gt-pl-2"> 70 70 {{.locale.Tr "settings.select_permissions"}} 71 71 </summary> 72 - <div class="activity meta"> 73 - <i>{{$.locale.Tr "settings.scoped_token_desc" (printf `href="/api/swagger" target="_blank"`) (printf `href="https://docs.gitea.com/development/oauth2-provider#scopes" target="_blank"`) | Str2html}}</i> 72 + <p class="activity meta"> 73 + <i>{{$.locale.Tr "settings.access_token_desc" (printf `href="/api/swagger" target="_blank"`) (printf `href="https://docs.gitea.com/development/oauth2-provider#scopes" target="_blank"`) | Str2html}}</i> 74 + </p> 75 + <div class="scoped-access-token-mount"> 76 + <scoped-access-token-selector 77 + :is-admin="{{if .IsAdmin}}true{{else}}false{{end}}" 78 + no-access-label="{{.locale.Tr "settings.permission_no_access"}}" 79 + read-label="{{.locale.Tr "settings.permission_read"}}" 80 + write-label="{{.locale.Tr "settings.permission_write"}}" 81 + ></scoped-access-token-selector> 74 82 </div> 75 - <scoped-access-token-category category="activitypub"></scoped-access-token-category> 76 - {{if .IsAdmin}} 77 - <scoped-access-token-category category="admin"></scoped-access-token-category> 78 - {{end}} 79 - <scoped-access-token-category category="issue"></scoped-access-token-category> 80 - <scoped-access-token-category category="misc"></scoped-access-token-category> 81 - <scoped-access-token-category category="notification"></scoped-access-token-category> 82 - <scoped-access-token-category category="organization"></scoped-access-token-category> 83 - <scoped-access-token-category category="package"></scoped-access-token-category> 84 - <scoped-access-token-category category="repository"></scoped-access-token-category> 85 - <scoped-access-token-category category="user"></scoped-access-token-category> 86 83 </details> 87 84 <div id="scoped-access-warning" class="ui warning message center gt-db gt-hidden"> 88 85 {{.locale.Tr "settings.at_least_one_permission"}}
+1
web_src/css/index.css
··· 1 1 @import "./modules/normalize.css"; 2 2 @import "./modules/animations.css"; 3 3 @import "./modules/button.css"; 4 + @import "./modules/select.css"; 4 5 @import "./modules/tippy.css"; 5 6 @import "./modules/modal.css"; 6 7 @import "./modules/breadcrumb.css";
+25
web_src/css/modules/select.css
··· 1 + .gitea-select { 2 + position: relative; 3 + } 4 + 5 + .gitea-select select { 6 + appearance: none; /* hide default triangle */ 7 + } 8 + 9 + /* ::before and ::after pseudo elements don't work on select elements, 10 + so we need to put it on the parent. */ 11 + .gitea-select::after { 12 + position: absolute; 13 + top: 12px; 14 + right: 8px; 15 + pointer-events: none; 16 + content: ''; 17 + width: 14px; 18 + height: 14px; 19 + mask-size: cover; 20 + -webkit-mask-size: cover; 21 + mask-image: var(--octicon-chevron-right); 22 + -webkit-mask-image: var(--octicon-chevron-right); 23 + transform: rotate(90deg); /* point the chevron down */ 24 + background: currentcolor; 25 + }
+81 -106
web_src/js/components/ScopedAccessTokenSelector.vue
··· 1 1 <template> 2 - <div class="scoped-access-token-category"> 3 - <div class="field gt-pl-2"> 4 - <label class="checkbox-label"> 5 - <input 6 - ref="category" 7 - v-model="categorySelected" 8 - class="scope-checkbox scoped-access-token-input" 9 - type="checkbox" 10 - name="scope" 11 - :value="'write:' + category" 12 - @input="onCategoryInput" 13 - > 14 - {{ category }} 15 - </label> 16 - </div> 17 - <div class="field gt-pl-4"> 18 - <div class="inline field"> 19 - <label class="checkbox-label"> 20 - <input 21 - ref="read" 22 - v-model="readSelected" 23 - :disabled="disableIndividual || writeSelected" 24 - class="scope-checkbox scoped-access-token-input" 25 - type="checkbox" 26 - name="scope" 27 - :value="'read:' + category" 28 - @input="onIndividualInput" 29 - > 30 - read:{{ category }} 31 - </label> 32 - </div> 33 - <div class="inline field"> 34 - <label class="checkbox-label"> 35 - <input 36 - ref="write" 37 - v-model="writeSelected" 38 - :disabled="disableIndividual" 39 - class="scope-checkbox scoped-access-token-input" 40 - type="checkbox" 41 - name="scope" 42 - :value="'write:' + category" 43 - @input="onIndividualInput" 44 - > 45 - write:{{ category }} 46 - </label> 47 - </div> 2 + <div v-for="category in categories" :key="category" class="field gt-pl-2 gt-pb-2 access-token-category"> 3 + <label class="category-label" :for="'access-token-scope-' + category"> 4 + {{ category }} 5 + </label> 6 + <div class="gitea-select"> 7 + <select 8 + class="ui selection access-token-select" 9 + name="scope" 10 + :id="'access-token-scope-' + category" 11 + > 12 + <option value=""> 13 + {{ noAccessLabel }} 14 + </option> 15 + <option :value="'read:' + category"> 16 + {{ readLabel }} 17 + </option> 18 + <option :value="'write:' + category"> 19 + {{ writeLabel }} 20 + </option> 21 + </select> 48 22 </div> 49 23 </div> 50 24 </template> 51 25 52 26 <script> 53 27 import {createApp} from 'vue'; 54 - import {showElem} from '../utils/dom.js'; 28 + import {hideElem, showElem} from '../utils/dom.js'; 55 29 56 30 const sfc = { 57 31 props: { 58 - category: { 32 + isAdmin: { 33 + type: Boolean, 34 + required: true, 35 + }, 36 + noAccessLabel: { 37 + type: String, 38 + required: true, 39 + }, 40 + readLabel: { 41 + type: String, 42 + required: true, 43 + }, 44 + writeLabel: { 59 45 type: String, 60 46 required: true, 61 47 }, 62 48 }, 63 49 64 - data: () => ({ 65 - categorySelected: false, 66 - disableIndividual: false, 67 - readSelected: false, 68 - writeSelected: false, 69 - }), 50 + computed: { 51 + categories() { 52 + const categories = [ 53 + 'activitypub', 54 + ]; 55 + if (this.isAdmin) { 56 + categories.push('admin'); 57 + } 58 + categories.push( 59 + 'issue', 60 + 'misc', 61 + 'notification', 62 + 'organization', 63 + 'package', 64 + 'repository', 65 + 'user'); 66 + return categories; 67 + } 68 + }, 69 + 70 + mounted() { 71 + document.getElementById('scoped-access-submit').addEventListener('click', this.onClickSubmit); 72 + }, 73 + 74 + unmounted() { 75 + document.getElementById('scoped-access-submit').removeEventListener('click', this.onClickSubmit); 76 + }, 70 77 71 78 methods: { 72 - /** 73 - * When entire category is toggled 74 - * @param {Event} e 75 - */ 76 - onCategoryInput(e) { 79 + onClickSubmit(e) { 77 80 e.preventDefault(); 78 - this.disableIndividual = this.$refs.category.checked; 79 - this.writeSelected = this.$refs.category.checked; 80 - this.readSelected = this.$refs.category.checked; 81 - }, 82 81 83 - /** 84 - * When an individual level of category is toggled 85 - * @param {Event} e 86 - */ 87 - onIndividualInput(e) { 88 - e.preventDefault(); 89 - if (this.$refs.write.checked) { 90 - this.readSelected = true; 82 + const warningEl = document.getElementById('scoped-access-warning'); 83 + // check that at least one scope has been selected 84 + for (const el of document.getElementsByClassName('access-token-select')) { 85 + if (el.value) { 86 + // Hide the error if it was visible from previous attempt. 87 + hideElem(warningEl); 88 + // Submit the form. 89 + document.getElementById('scoped-access-form').submit(); 90 + // Don't show the warning. 91 + return; 92 + } 91 93 } 92 - this.categorySelected = this.$refs.write.checked; 93 - }, 94 - } 94 + // no scopes selected, show validation error 95 + showElem(warningEl); 96 + } 97 + }, 95 98 }; 96 99 97 100 export default sfc; ··· 100 103 * Initialize category toggle sections 101 104 */ 102 105 export function initScopedAccessTokenCategories() { 103 - for (const el of document.getElementsByTagName('scoped-access-token-category')) { 104 - const category = el.getAttribute('category'); 105 - createApp(sfc, { 106 - category, 107 - }).mount(el); 106 + for (const el of document.getElementsByClassName('scoped-access-token-mount')) { 107 + createApp({}) 108 + .component('scoped-access-token-selector', sfc) 109 + .mount(el); 108 110 } 109 - 110 - document.getElementById('scoped-access-submit')?.addEventListener('click', (e) => { 111 - e.preventDefault(); 112 - // check that at least one scope has been selected 113 - for (const el of document.getElementsByClassName('scoped-access-token-input')) { 114 - if (el.checked) { 115 - document.getElementById('scoped-access-form').submit(); 116 - } 117 - } 118 - // no scopes selected, show validation error 119 - showElem(document.getElementById('scoped-access-warning')); 120 - }); 121 111 } 122 112 123 113 </script> 124 - 125 - <style scoped> 126 - .scoped-access-token-category { 127 - padding-top: 10px; 128 - padding-bottom: 10px; 129 - } 130 - 131 - .checkbox-label { 132 - cursor: pointer; 133 - } 134 - 135 - .scope-checkbox { 136 - margin: 4px 5px 0 0; 137 - } 138 - </style>