Просмотр исходного кода

Feature: Desktop view - Implement Token Access personal setting section.

Co-authored-by: Dominik Klein <dk@zammad.com>
Co-authored-by: Mantas Masalskis <mm@zammad.com>
Co-authored-by: Dusan Vuckovic <dv@zammad.com>
Dominik Klein 10 месяцев назад
Родитель
Сommit
58da12c730

+ 14 - 0
app/assets/javascripts/app/controllers/_profile/token_access.coffee

@@ -81,6 +81,8 @@ class Create extends App.ControllerModal
   buttonSubmit: __('Create')
   buttonCancel: true
   shown: true
+  events:
+    'change input[name=permission]': 'onToggle'
 
   content: ->
     content = $(App.view('profile/token_access_create')(
@@ -93,6 +95,12 @@ class Create extends App.ControllerModal
     content.find('.js-date').html(datepicker)
     content
 
+  onToggle: (e) =>
+    isChecked = e.currentTarget.checked
+    prefix    = e.currentTarget.value + '.'
+
+    @$(".js-subPermissionList:has(input[value^='#{prefix}'])").toggle(!isChecked)
+
   onSubmit: (e) =>
     e.preventDefault()
     params = @formParam(e.target)
@@ -108,6 +116,12 @@ class Create extends App.ControllerModal
     if !_.isArray(params['permission'])
       params['permission'] = [params['permission']]
 
+    params['permission'] = _.reduce(params['permission'], (memo, permissionName) ->
+      startsWithRegex = '^'+permissionName + '\\.'
+
+      _.filter(memo, (elem) -> !elem.match(startsWithRegex))
+    , params['permission'])
+
     @ajax(
       id:          'user_access_token_create'
       type:        'POST'

+ 1 - 1
app/assets/javascripts/app/models/permission.coffee

@@ -1,4 +1,4 @@
 class App.Permission extends App.Model
-  @configure 'Permission', 'name', 'note', 'active', 'preferences'
+  @configure 'Permission', 'name', 'description', 'active', 'preferences'
   @extend Spine.Model.Ajax
   @url: @apiPath + '/permissions'

+ 16 - 4
app/assets/javascripts/app/views/generic/permission.jst.eco

@@ -5,8 +5,14 @@
         <input type="checkbox" value="<%= permission.id %>" name="permission_ids" <% if _.contains(@params.permission_ids, permission.id): %>checked<% end %> <% if permission.preferences.disabled: %>disabled<% end %> data-permission-name="<%= permission.name %>"/>
         <%- @Icon('checkbox', 'icon-unchecked') %>
         <%- @Icon('checkbox-checked', 'icon-checked') %>
-        <span class="label-text"><%= permission.displayName() %></span>
-        <span class="help-text"><%- @T(permission.note, @T(permission.preferences.translations)) %></span>
+        <span class="label-text">
+          <% - if permission.label: %>
+            <%- @T(permission.label) %> (<%= permission.name %>)
+          <% else: %>
+            <%= permission.name %>
+          <% end %>
+        </span>
+        <span class="help-text"><%- @T(permission.description) %></span>
       </label>
     <% else: %>
       <div class="checkbox-child js-subPermissionList">
@@ -14,8 +20,14 @@
           <input type="checkbox" value="<%= permission.id %>" name="permission_ids" <% if _.contains(@params.permission_ids, permission.id): %>checked<% end %> <% if permission.preferences.disabled: %>disabled<% end %> data-permission-name="<%= permission.name %>"/>
           <%- @Icon('checkbox', 'icon-unchecked') %>
           <%- @Icon('checkbox-checked', 'icon-checked') %>
-          <span class="label-text"><%= permission.displayName().replace(/^.+?\./, '') %></span>
-          <span class="help-text"><%- @T(permission.note, @T(permission.preferences.translations)) %></span>
+          <span class="label-text">
+            <% - if permission.label: %>
+              <%- @T(permission.label) %> (<%= permission.name %>)
+            <% else: %>
+              <%= permission.name %>
+            <% end %>
+          </span>
+          <span class="help-text"><%- @T(permission.description) %></span>
         </label>
       </div>
     <% end %>

+ 17 - 5
app/assets/javascripts/app/views/profile/token_access_create.jst.eco

@@ -13,15 +13,21 @@
 </div>
 
 <div class="permission form-group checkbox">
-  <div class="checkbox">
+  <div class="controls checkbox">
   <% for permission in @permissions: %>
     <% if !permission.name.match(/\./): %>
     <label class="inline-label checkbox-replacement">
       <input type="checkbox" value="<%= permission.name %>" name="permission" <% if @params && _.contains(@params.permissions, permission.id): %>checked<% end %> <% if permission.preferences.disabled: %>disabled<% end %>/>
       <%- @Icon('checkbox', 'icon-unchecked') %>
       <%- @Icon('checkbox-checked', 'icon-checked') %>
-      <span class="label-text"><%= permission.name %></span>
-      <span class="help-text"><%- @T(permission.note, @T(permission.preferences.translations)) %></span>
+      <span class="label-text">
+        <% - if permission.label: %>
+          <%- @T(permission.label) %> (<%= permission.name %>)
+        <% else: %>
+          <%= permission.name %>
+        <% end %>
+      </span>
+      <span class="help-text"><%- @T(permission.description) %></span>
     </label>
     <% else: %>
       <div class="checkbox-child js-subPermissionList">
@@ -29,8 +35,14 @@
           <input type="checkbox" value="<%= permission.name %>" name="permission" <% if @params && _.contains(@params.permissions, permission.id): %>checked<% end %> <% if permission.preferences.disabled: %>disabled<% end %>/>
           <%- @Icon('checkbox', 'icon-unchecked') %>
           <%- @Icon('checkbox-checked', 'icon-checked') %>
-          <span class="label-text"><%= permission.name.replace(/^.+?\./, '') %></span>
-          <span class="help-text"><%- @T(permission.note, @T(permission.preferences.translations)) %></span>
+          <span class="label-text">
+            <% - if permission.label: %>
+              <%- @T(permission.label) %> (<%= permission.name %>)
+            <% else: %>
+              <%= permission.name %>
+            <% end %>
+          </span>
+          <span class="help-text"><%- @T(permission.description) %></span>
         </label>
       </div>
     <% end %>

+ 8 - 30
app/controllers/user_access_token_controller.rb

@@ -27,28 +27,12 @@ curl http://localhost/api/v1/user_access_token -v -u #{login}:#{password}
 =end
 
   def index
-    tokens = Token.select(Token.column_names - %w[persistent token])
-                  .where(action: 'api', persistent: true, user_id: current_user.id)
-                  .reorder(updated_at: :desc, name: :asc)
-
-    base_query       = Permission.reorder(:name).where(active: true)
-    permission_names = current_user.permissions.pluck(:name)
-    ancestor_names   = permission_names.flat_map { |name| Permission.with_parents(name) }.uniq -
-                       permission_names
-    descendant_names = permission_names.map { |name| "#{SqlHelper.quote_like(name)}.%" }
-
-    permissions = base_query.where(name: [*ancestor_names, *permission_names])
-
-    descendant_names.each do |name|
-      permissions = permissions.or(base_query.where('permissions.name LIKE ?', name))
-    end
-
-    permissions.select { |permission| permission.name.in?(ancestor_names) }
-               .each { |permission| permission.preferences['disabled'] = true }
+    tokens      = Service::User::AccessToken::List.new(current_user).execute
+    permissions = current_user.permissions_with_child_and_parent_elements
 
     render json: {
-      tokens:      tokens.map(&:attributes),
-      permissions: permissions.map(&:attributes),
+      tokens:      tokens,
+      permissions: permissions,
     }, status: :ok
   end
 
@@ -82,16 +66,10 @@ curl http://localhost/api/v1/user_access_token -v -u #{login}:#{password} -H "Co
       raise Exceptions::UnprocessableEntity, __("The required parameter 'name' is missing.")
     end
 
-    token = Token.create!(
-      action:      'api',
-      name:        params[:name],
-      persistent:  true,
-      user_id:     current_user.id,
-      expires_at:  params[:expires_at],
-      preferences: {
-        permission: params[:permission]
-      }
-    )
+    token = Service::User::AccessToken::Create
+      .new(current_user, **params.permit(:name, :expires_at, permission: []).to_h.to_options)
+      .execute
+
     render json: {
       token: token.token,
     }, status: :ok

+ 1 - 0
app/frontend/apps/desktop/components/CommonBreadcrumb/CommonBreadcrumb.vue

@@ -10,6 +10,7 @@ defineProps<{
 }>()
 
 const locale = useLocaleStore()
+// TODO: Missing handling when there is not enough space for the breadcrumb
 </script>
 
 <template>

+ 1 - 1
app/frontend/apps/desktop/components/CommonDialog/CommonDialog.vue

@@ -78,7 +78,7 @@ onMounted(() => {
   <CommonOverlayContainer
     :id="dialogId"
     tag="div"
-    class="fixed top-[50%] z-50 min-w-[500px] translate-y-[-50%] ltr:left-[50%] ltr:translate-x-[-50%] rtl:right-[50%] rtl:-translate-x-[-50%]"
+    class="fixed top-[50%] z-50 w-[500px] translate-y-[-50%] ltr:left-[50%] ltr:translate-x-[-50%] rtl:right-[50%] rtl:-translate-x-[-50%]"
     role="dialog"
     :aria-labelledby="`${dialogId}-title`"
     @click-background="close()"

+ 2 - 0
app/frontend/apps/desktop/components/CommonDialog/CommonDialogActionFooter.vue

@@ -9,6 +9,7 @@ export interface Props {
   hideActionButton?: boolean
   actionLabel?: string
   actionButton?: Pick<ButtonProps, 'prefixIcon' | 'variant'>
+  hideCancelButton?: boolean
   cancelLabel?: string
   cancelButton?: Pick<ButtonProps, 'prefixIcon' | 'variant'>
 }
@@ -38,6 +39,7 @@ const action = () => {
     class="flex items-center gap-2 ltr:justify-end rtl:flex-row-reverse rtl:justify-start"
   >
     <CommonButton
+      v-if="!hideCancelButton"
       size="large"
       :prefix-icon="cancelButton?.prefixIcon"
       :variant="cancelButton?.variant || 'secondary'"

+ 5 - 7
app/frontend/apps/desktop/components/CommonFlyout/CommonFlyout.vue

@@ -19,19 +19,16 @@ import {
 } from '@vueuse/core'
 
 import { getFirstFocusableElement } from '#shared/utils/getFocusableElements.ts'
+import stopEvent from '#shared/utils/events.ts'
 
 import ResizeHandle from '#desktop/components/ResizeHandle/ResizeHandle.vue'
 import CommonButton from '#desktop/components/CommonButton/CommonButton.vue'
 import CommonOverlayContainer from '#desktop/components/CommonOverlayContainer/CommonOverlayContainer.vue'
 import { useResizeWidthHandle } from '#desktop/components/ResizeHandle/composables/useResizeWidthHandle.ts'
-import type { FlyoutSizes } from '#desktop/components/CommonFlyout/types.ts'
 
-import stopEvent from '#shared/utils/events.ts'
+import type { ActionFooterOptions, FlyoutSizes } from './types.ts'
 import { closeFlyout } from './useFlyout.ts'
-
-import CommonFlyoutActionFooter, {
-  type Props as ActionFooterProps,
-} from './CommonFlyoutActionFooter.vue'
+import CommonFlyoutActionFooter from './CommonFlyoutActionFooter.vue'
 
 export interface Props {
   /**
@@ -55,7 +52,7 @@ export interface Props {
   noCloseOnBackdropClick?: boolean
   noCloseOnEscape?: boolean
   hideFooter?: boolean
-  footerActionOptions?: ActionFooterProps
+  footerActionOptions?: ActionFooterOptions
   noCloseOnAction?: boolean
   /**
    * @property noAutofocus
@@ -86,6 +83,7 @@ const close = async () => {
   await closeFlyout(props.name)
 }
 
+// TODO: maybe we could add a better handling in combination with a form....
 const action = async () => {
   emit('action')
 

+ 4 - 17
app/frontend/apps/desktop/components/CommonFlyout/CommonFlyoutActionFooter.vue

@@ -1,25 +1,12 @@
 <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
 
 <script setup lang="ts">
-import CommonButton, {
-  type Props as ButtonProps,
-} from '#desktop/components/CommonButton/CommonButton.vue'
-import { useForm } from '#shared/components/Form/useForm.ts'
 import { toRef } from 'vue'
-import type { FormRef } from '#shared/components/Form/types.ts'
 
-export interface Props {
-  hideActionButton?: boolean
-  actionLabel?: string
-  actionButton?: Pick<
-    ButtonProps,
-    'prefixIcon' | 'variant' | 'type' | 'disabled'
-  >
-  hideCancelButton?: boolean
-  cancelLabel?: string
-  cancelButton?: Pick<ButtonProps, 'prefixIcon' | 'variant' | 'disabled'>
-  form?: FormRef
-}
+import { useForm } from '#shared/components/Form/useForm.ts'
+
+import CommonButton from '#desktop/components/CommonButton/CommonButton.vue'
+import type { ActionFooterOptions as Props } from './types.ts'
 
 const props = withDefaults(defineProps<Props>(), {
   actionLabel: __('Update'),

Некоторые файлы не были показаны из-за большого количества измененных файлов