forked from gitea/gitea
		
	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_access_all = All (public, private, and limited) | ||||
| 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 | ||||
| permissions_list = Permissions: | ||||
| 
 | ||||
|  | ||||
| @ -69,20 +69,17 @@ | ||||
| 					<summary class="gt-pb-4 gt-pl-2"> | ||||
| 						{{.locale.Tr "settings.select_permissions"}} | ||||
| 					</summary> | ||||
| 					<div 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> | ||||
| 					<p class="activity meta"> | ||||
| 						<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> | ||||
| 					<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> | ||||
| 				<div id="scoped-access-warning" class="ui warning message center gt-db gt-hidden"> | ||||
| 					{{.locale.Tr "settings.at_least_one_permission"}} | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| @import "./modules/normalize.css"; | ||||
| @import "./modules/animations.css"; | ||||
| @import "./modules/button.css"; | ||||
| @import "./modules/select.css"; | ||||
| @import "./modules/tippy.css"; | ||||
| @import "./modules/modal.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> | ||||
|   <div class="scoped-access-token-category"> | ||||
|     <div class="field gt-pl-2"> | ||||
|       <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 }} | ||||
|       </label> | ||||
|     </div> | ||||
|     <div class="field gt-pl-4"> | ||||
|       <div class="inline field"> | ||||
|         <label class="checkbox-label"> | ||||
|           <input | ||||
|             ref="read" | ||||
|             v-model="readSelected" | ||||
|             :disabled="disableIndividual || writeSelected" | ||||
|             class="scope-checkbox scoped-access-token-input" | ||||
|             type="checkbox" | ||||
|             name="scope" | ||||
|             :value="'read:' + category" | ||||
|             @input="onIndividualInput" | ||||
|           > | ||||
|           read:{{ category }} | ||||
|         </label> | ||||
|       </div> | ||||
|       <div class="inline field"> | ||||
|         <label class="checkbox-label"> | ||||
|           <input | ||||
|             ref="write" | ||||
|             v-model="writeSelected" | ||||
|             :disabled="disableIndividual" | ||||
|             class="scope-checkbox scoped-access-token-input" | ||||
|             type="checkbox" | ||||
|             name="scope" | ||||
|             :value="'write:' + category" | ||||
|             @input="onIndividualInput" | ||||
|           > | ||||
|           write:{{ category }} | ||||
|         </label> | ||||
|       </div> | ||||
|   <div v-for="category in categories" :key="category" class="field gt-pl-2 gt-pb-2 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> | ||||
| 
 | ||||
| <script> | ||||
| import {createApp} from 'vue'; | ||||
| import {showElem} from '../utils/dom.js'; | ||||
| import {hideElem, showElem} from '../utils/dom.js'; | ||||
| 
 | ||||
| const sfc = { | ||||
|   props: { | ||||
|     category: { | ||||
|     isAdmin: { | ||||
|       type: Boolean, | ||||
|       required: true, | ||||
|     }, | ||||
|     noAccessLabel: { | ||||
|       type: String, | ||||
|       required: true, | ||||
|     }, | ||||
|     readLabel: { | ||||
|       type: String, | ||||
|       required: true, | ||||
|     }, | ||||
|     writeLabel: { | ||||
|       type: String, | ||||
|       required: true, | ||||
|     }, | ||||
|   }, | ||||
| 
 | ||||
|   data: () => ({ | ||||
|     categorySelected: false, | ||||
|     disableIndividual: false, | ||||
|     readSelected: false, | ||||
|     writeSelected: false, | ||||
|   }), | ||||
|   computed: { | ||||
|     categories() { | ||||
|       const categories = [ | ||||
|         'activitypub', | ||||
|       ]; | ||||
|       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: { | ||||
|     /** | ||||
|      * When entire category is toggled | ||||
|      * @param {Event} e | ||||
|      */ | ||||
|     onCategoryInput(e) { | ||||
|     onClickSubmit(e) { | ||||
|       e.preventDefault(); | ||||
|       this.disableIndividual = this.$refs.category.checked; | ||||
|       this.writeSelected = this.$refs.category.checked; | ||||
|       this.readSelected = this.$refs.category.checked; | ||||
|     }, | ||||
| 
 | ||||
|     /** | ||||
|      * When an individual level of category is toggled | ||||
|      * @param {Event} e | ||||
|      */ | ||||
|     onIndividualInput(e) { | ||||
|       e.preventDefault(); | ||||
|       if (this.$refs.write.checked) { | ||||
|         this.readSelected = true; | ||||
|       const warningEl = document.getElementById('scoped-access-warning'); | ||||
|       // check that at least one scope has been selected | ||||
|       for (const el of document.getElementsByClassName('access-token-select')) { | ||||
|         if (el.value) { | ||||
|           // Hide the error if it was visible from previous attempt. | ||||
|           hideElem(warningEl); | ||||
|           // Submit the form. | ||||
|           document.getElementById('scoped-access-form').submit(); | ||||
|           // Don't show the warning. | ||||
|           return; | ||||
|         } | ||||
|       } | ||||
|       this.categorySelected = this.$refs.write.checked; | ||||
|     }, | ||||
|   } | ||||
|       // no scopes selected, show validation error | ||||
|       showElem(warningEl); | ||||
|     } | ||||
|   }, | ||||
| }; | ||||
| 
 | ||||
| export default sfc; | ||||
| @ -100,39 +103,11 @@ export default sfc; | ||||
|  * Initialize category toggle sections | ||||
|  */ | ||||
| export function initScopedAccessTokenCategories() { | ||||
|   for (const el of document.getElementsByTagName('scoped-access-token-category')) { | ||||
|     const category = el.getAttribute('category'); | ||||
|     createApp(sfc, { | ||||
|       category, | ||||
|     }).mount(el); | ||||
|   for (const el of document.getElementsByClassName('scoped-access-token-mount')) { | ||||
|     createApp({}) | ||||
|       .component('scoped-access-token-selector', sfc) | ||||
|       .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> | ||||
| 
 | ||||
| <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
	 Jonathan Tran
						Jonathan Tran