mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 00:54:43 +01:00 
			
		
		
		
	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">
This commit is contained in:
		
							parent
							
								
									f62cd2f473
								
							
						
					
					
						commit
						a583c56306
					
				| @ -811,7 +811,10 @@ repo_and_org_access = Repository and Organization Access | |||||||
| permissions_public_only = Public only | permissions_public_only = Public only | ||||||
| permissions_access_all = All (public, private, and limited) | permissions_access_all = All (public, private, and limited) | ||||||
| select_permissions = Select permissions | select_permissions = Select permissions | ||||||
| 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. | permission_no_access = No Access | ||||||
|  | permission_read = Read | ||||||
|  | permission_write = Read and Write | ||||||
|  | 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. | ||||||
| at_least_one_permission = You must select at least one permission to create a token | at_least_one_permission = You must select at least one permission to create a token | ||||||
| permissions_list = Permissions: | permissions_list = Permissions: | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -69,20 +69,17 @@ | |||||||
| 					<summary class="gt-pb-4 gt-pl-2"> | 					<summary class="gt-pb-4 gt-pl-2"> | ||||||
| 						{{.locale.Tr "settings.select_permissions"}} | 						{{.locale.Tr "settings.select_permissions"}} | ||||||
| 					</summary> | 					</summary> | ||||||
| 					<div class="activity meta"> | 					<p class="activity meta"> | ||||||
| 						<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> | 						<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> | ||||||
|  | 					</p> | ||||||
|  | 					<div class="scoped-access-token-mount"> | ||||||
|  | 						<scoped-access-token-selector | ||||||
|  | 							:is-admin="{{if .IsAdmin}}true{{else}}false{{end}}" | ||||||
|  | 							no-access-label="{{.locale.Tr "settings.permission_no_access"}}" | ||||||
|  | 							read-label="{{.locale.Tr "settings.permission_read"}}" | ||||||
|  | 							write-label="{{.locale.Tr "settings.permission_write"}}" | ||||||
|  | 						></scoped-access-token-selector> | ||||||
| 					</div> | 					</div> | ||||||
| 					<scoped-access-token-category category="activitypub"></scoped-access-token-category> |  | ||||||
| 					{{if .IsAdmin}} |  | ||||||
| 						<scoped-access-token-category category="admin"></scoped-access-token-category> |  | ||||||
| 					{{end}} |  | ||||||
| 					<scoped-access-token-category category="issue"></scoped-access-token-category> |  | ||||||
| 					<scoped-access-token-category category="misc"></scoped-access-token-category> |  | ||||||
| 					<scoped-access-token-category category="notification"></scoped-access-token-category> |  | ||||||
| 					<scoped-access-token-category category="organization"></scoped-access-token-category> |  | ||||||
| 					<scoped-access-token-category category="package"></scoped-access-token-category> |  | ||||||
| 					<scoped-access-token-category category="repository"></scoped-access-token-category> |  | ||||||
| 					<scoped-access-token-category category="user"></scoped-access-token-category> |  | ||||||
| 				</details> | 				</details> | ||||||
| 				<div id="scoped-access-warning" class="ui warning message center gt-db gt-hidden"> | 				<div id="scoped-access-warning" class="ui warning message center gt-db gt-hidden"> | ||||||
| 					{{.locale.Tr "settings.at_least_one_permission"}} | 					{{.locale.Tr "settings.at_least_one_permission"}} | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| @import "./modules/normalize.css"; | @import "./modules/normalize.css"; | ||||||
| @import "./modules/animations.css"; | @import "./modules/animations.css"; | ||||||
| @import "./modules/button.css"; | @import "./modules/button.css"; | ||||||
|  | @import "./modules/select.css"; | ||||||
| @import "./modules/tippy.css"; | @import "./modules/tippy.css"; | ||||||
| @import "./modules/modal.css"; | @import "./modules/modal.css"; | ||||||
| @import "./modules/breadcrumb.css"; | @import "./modules/breadcrumb.css"; | ||||||
|  | |||||||
							
								
								
									
										25
									
								
								web_src/css/modules/select.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								web_src/css/modules/select.css
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,25 @@ | |||||||
|  | .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,97 +1,100 @@ | |||||||
| <template> | <template> | ||||||
|   <div class="scoped-access-token-category"> |   <div v-for="category in categories" :key="category" class="field gt-pl-2 gt-pb-2 access-token-category"> | ||||||
|     <div class="field gt-pl-2"> |     <label class="category-label" :for="'access-token-scope-' + category"> | ||||||
|       <label class="checkbox-label"> |  | ||||||
|         <input |  | ||||||
|           ref="category" |  | ||||||
|           v-model="categorySelected" |  | ||||||
|           class="scope-checkbox scoped-access-token-input" |  | ||||||
|           type="checkbox" |  | ||||||
|           name="scope" |  | ||||||
|           :value="'write:' + category" |  | ||||||
|           @input="onCategoryInput" |  | ||||||
|         > |  | ||||||
|       {{ category }} |       {{ category }} | ||||||
|     </label> |     </label> | ||||||
|     </div> |     <div class="gitea-select"> | ||||||
|     <div class="field gt-pl-4"> |       <select | ||||||
|       <div class="inline field"> |         class="ui selection access-token-select" | ||||||
|         <label class="checkbox-label"> |  | ||||||
|           <input |  | ||||||
|             ref="read" |  | ||||||
|             v-model="readSelected" |  | ||||||
|             :disabled="disableIndividual || writeSelected" |  | ||||||
|             class="scope-checkbox scoped-access-token-input" |  | ||||||
|             type="checkbox" |  | ||||||
|         name="scope" |         name="scope" | ||||||
|             :value="'read:' + category" |         :id="'access-token-scope-' + category" | ||||||
|             @input="onIndividualInput" |  | ||||||
|       > |       > | ||||||
|           read:{{ category }} |         <option value=""> | ||||||
|         </label> |           {{ noAccessLabel }} | ||||||
|       </div> |         </option> | ||||||
|       <div class="inline field"> |         <option :value="'read:' + category"> | ||||||
|         <label class="checkbox-label"> |           {{ readLabel }} | ||||||
|           <input |         </option> | ||||||
|             ref="write" |         <option :value="'write:' + category"> | ||||||
|             v-model="writeSelected" |           {{ writeLabel }} | ||||||
|             :disabled="disableIndividual" |         </option> | ||||||
|             class="scope-checkbox scoped-access-token-input" |       </select> | ||||||
|             type="checkbox" |  | ||||||
|             name="scope" |  | ||||||
|             :value="'write:' + category" |  | ||||||
|             @input="onIndividualInput" |  | ||||||
|           > |  | ||||||
|           write:{{ category }} |  | ||||||
|         </label> |  | ||||||
|       </div> |  | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script> | <script> | ||||||
| import {createApp} from 'vue'; | import {createApp} from 'vue'; | ||||||
| import {showElem} from '../utils/dom.js'; | import {hideElem, showElem} from '../utils/dom.js'; | ||||||
| 
 | 
 | ||||||
| const sfc = { | const sfc = { | ||||||
|   props: { |   props: { | ||||||
|     category: { |     isAdmin: { | ||||||
|  |       type: Boolean, | ||||||
|  |       required: true, | ||||||
|  |     }, | ||||||
|  |     noAccessLabel: { | ||||||
|  |       type: String, | ||||||
|  |       required: true, | ||||||
|  |     }, | ||||||
|  |     readLabel: { | ||||||
|  |       type: String, | ||||||
|  |       required: true, | ||||||
|  |     }, | ||||||
|  |     writeLabel: { | ||||||
|       type: String, |       type: String, | ||||||
|       required: true, |       required: true, | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
| 
 | 
 | ||||||
|   data: () => ({ |   computed: { | ||||||
|     categorySelected: false, |     categories() { | ||||||
|     disableIndividual: false, |       const categories = [ | ||||||
|     readSelected: false, |         'activitypub', | ||||||
|     writeSelected: false, |       ]; | ||||||
|   }), |       if (this.isAdmin) { | ||||||
|  |         categories.push('admin'); | ||||||
|  |       } | ||||||
|  |       categories.push( | ||||||
|  |         'issue', | ||||||
|  |         'misc', | ||||||
|  |         'notification', | ||||||
|  |         'organization', | ||||||
|  |         'package', | ||||||
|  |         'repository', | ||||||
|  |         'user'); | ||||||
|  |       return categories; | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  | 
 | ||||||
|  |   mounted() { | ||||||
|  |     document.getElementById('scoped-access-submit').addEventListener('click', this.onClickSubmit); | ||||||
|  |   }, | ||||||
|  | 
 | ||||||
|  |   unmounted() { | ||||||
|  |     document.getElementById('scoped-access-submit').removeEventListener('click', this.onClickSubmit); | ||||||
|  |   }, | ||||||
| 
 | 
 | ||||||
|   methods: { |   methods: { | ||||||
|     /** |     onClickSubmit(e) { | ||||||
|      * When entire category is toggled |  | ||||||
|      * @param {Event} e |  | ||||||
|      */ |  | ||||||
|     onCategoryInput(e) { |  | ||||||
|       e.preventDefault(); |       e.preventDefault(); | ||||||
|       this.disableIndividual = this.$refs.category.checked; |  | ||||||
|       this.writeSelected = this.$refs.category.checked; |  | ||||||
|       this.readSelected = this.$refs.category.checked; |  | ||||||
|     }, |  | ||||||
| 
 | 
 | ||||||
|     /** |       const warningEl = document.getElementById('scoped-access-warning'); | ||||||
|      * When an individual level of category is toggled |       // check that at least one scope has been selected | ||||||
|      * @param {Event} e |       for (const el of document.getElementsByClassName('access-token-select')) { | ||||||
|      */ |         if (el.value) { | ||||||
|     onIndividualInput(e) { |           // Hide the error if it was visible from previous attempt. | ||||||
|       e.preventDefault(); |           hideElem(warningEl); | ||||||
|       if (this.$refs.write.checked) { |           // Submit the form. | ||||||
|         this.readSelected = true; |           document.getElementById('scoped-access-form').submit(); | ||||||
|  |           // Don't show the warning. | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       // no scopes selected, show validation error | ||||||
|  |       showElem(warningEl); | ||||||
|     } |     } | ||||||
|       this.categorySelected = this.$refs.write.checked; |  | ||||||
|   }, |   }, | ||||||
|   } |  | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export default sfc; | export default sfc; | ||||||
| @ -100,39 +103,11 @@ export default sfc; | |||||||
|  * Initialize category toggle sections |  * Initialize category toggle sections | ||||||
|  */ |  */ | ||||||
| export function initScopedAccessTokenCategories() { | export function initScopedAccessTokenCategories() { | ||||||
|   for (const el of document.getElementsByTagName('scoped-access-token-category')) { |   for (const el of document.getElementsByClassName('scoped-access-token-mount')) { | ||||||
|     const category = el.getAttribute('category'); |     createApp({}) | ||||||
|     createApp(sfc, { |       .component('scoped-access-token-selector', sfc) | ||||||
|       category, |       .mount(el); | ||||||
|     }).mount(el); |  | ||||||
|   } |   } | ||||||
| 
 |  | ||||||
|   document.getElementById('scoped-access-submit')?.addEventListener('click', (e) => { |  | ||||||
|     e.preventDefault(); |  | ||||||
|     // check that at least one scope has been selected |  | ||||||
|     for (const el of document.getElementsByClassName('scoped-access-token-input')) { |  | ||||||
|       if (el.checked) { |  | ||||||
|         document.getElementById('scoped-access-form').submit(); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     // no scopes selected, show validation error |  | ||||||
|     showElem(document.getElementById('scoped-access-warning')); |  | ||||||
|   }); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| </script> | </script> | ||||||
| 
 |  | ||||||
| <style scoped> |  | ||||||
| .scoped-access-token-category { |  | ||||||
|   padding-top: 10px; |  | ||||||
|   padding-bottom: 10px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .checkbox-label { |  | ||||||
|   cursor: pointer; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .scope-checkbox { |  | ||||||
|   margin: 4px 5px 0 0; |  | ||||||
| } |  | ||||||
| </style> |  | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user