Skip to content

Commit

Permalink
Feature: Desktop - Add form field permissions.
Browse files Browse the repository at this point in the history
  • Loading branch information
dvuckovic committed May 3, 2024
1 parent 3823bb5 commit e42f4d9
Show file tree
Hide file tree
Showing 15 changed files with 1,063 additions and 147 deletions.
59 changes: 11 additions & 48 deletions app/frontend/apps/desktop/components/CommonSelect/CommonSelect.vue
Expand Up @@ -29,6 +29,7 @@ import type { AutoCompleteOption } from '#shared/components/Form/fields/FieldAut
import testFlags from '#shared/utils/testFlags.ts'
import CommonLabel from '#shared/components/CommonLabel/CommonLabel.vue'
import { i18n } from '#shared/i18n.ts'
import { useTransitionCollapse } from '#desktop/composables/useTransitionCollapse.ts'
import CommonSelectItem from './CommonSelectItem.vue'
import { useCommonSelect } from './useCommonSelect.ts'
import type { CommonSelectInternalInstance } from './types.ts'
Expand Down Expand Up @@ -305,7 +306,8 @@ const emptyLabelText = computed(() => {
return props.filter ? __('No results found') : __('Start typing to search…')
})
const duration = VITE_TEST_MODE ? undefined : { enter: 300, leave: 200 }
const { collapseDuration, collapseEnter, collapseAfterEnter, collapseLeave } =
useTransitionCollapse()
</script>

<template>
Expand All @@ -316,22 +318,21 @@ const duration = VITE_TEST_MODE ? undefined : { enter: 300, leave: 200 }
:focus="moveFocusToDropdown"
/>
<Teleport to="body">
<Transition :duration="duration">
<Transition
name="collapse"
:duration="collapseDuration"
@enter="collapseEnter"
@after-enter="collapseAfterEnter"
@leave="collapseLeave"
>
<div
v-if="showDropdown"
id="common-select"
ref="dropdownElement"
class="fixed z-10 flex min-h-9 antialiased"
:style="dropdownStyle"
>
<div
class="select-dialog w-full"
role="menu"
:class="{
'select-dialog--up': hasDirectionUp,
'select-dialog--down': !hasDirectionUp,
}"
>
<div class="w-full" role="menu">
<div
class="flex h-full flex-col items-start border-x border-neutral-100 bg-white dark:border-gray-900 dark:bg-gray-500"
:class="{
Expand Down Expand Up @@ -395,41 +396,3 @@ const duration = VITE_TEST_MODE ? undefined : { enter: 300, leave: 200 }
</Transition>
</Teleport>
</template>

<style scoped>
.select-dialog {
&--down {
@apply origin-top;
}
&--up {
@apply origin-bottom;
}
}
.v-enter-active {
.select-dialog {
@apply duration-200 ease-out;
}
}
.v-leave-active {
.select-dialog {
@apply duration-200 ease-in;
}
}
.v-enter-to,
.v-leave-from {
.select-dialog {
@apply scale-y-100 opacity-100;
}
}
.v-enter-from,
.v-leave-to {
.select-dialog {
@apply scale-y-50 opacity-0;
}
}
</style>
Expand Up @@ -457,7 +457,7 @@ useFormBlock(contextReactive, openSelectDropdown)
</span>
<CommonIcon
:aria-label="i18n.t('Unselect Option')"
class="shrink-0 fill-stone-200 hover:fill-black focus-visible:rounded-sm focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-1 focus-visible:outline-blue-800 dark:fill-neutral-500 dark:hover:fill-white"
class="shrink-0 fill-stone-200 hover:fill-black focus:outline-none focus-visible:rounded-sm focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-1 focus-visible:outline-blue-800 dark:fill-neutral-500 dark:hover:fill-white"
name="x-lg"
size="xs"
role="button"
Expand Down Expand Up @@ -520,7 +520,7 @@ useFormBlock(contextReactive, openSelectDropdown)
<CommonIcon
v-if="context.clearable && hasValue && !context.disabled"
:aria-label="i18n.t('Clear Selection')"
class="shrink-0 fill-stone-200 hover:fill-black focus-visible:rounded-sm focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-1 focus-visible:outline-blue-800 dark:fill-neutral-500 dark:hover:fill-white"
class="shrink-0 fill-stone-200 hover:fill-black focus:outline-none focus-visible:rounded-sm focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-1 focus-visible:outline-blue-800 dark:fill-neutral-500 dark:hover:fill-white"
name="x-lg"
size="xs"
role="button"
Expand Down
@@ -0,0 +1,218 @@
<!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->

<script setup lang="ts">
import { computed, ref, toRef } from 'vue'
import { cloneDeep } from 'lodash-es'
import { useTransitionCollapse } from '#desktop/composables/useTransitionCollapse.ts'
import useValue from '#shared/components/Form/composables/useValue.ts'
import { i18n } from '#shared/i18n.ts'
import { useDelegateFocus } from '#shared/composables/useDelegateFocus.ts'
import type { PermissionsChildOption, PermissionsProps } from './types.ts'
const props = defineProps<{
context: PermissionsProps
}>()
const contextReactive = toRef(props, 'context')
const { localValue } = useValue(contextReactive)
const valueLookup = computed<Record<string, boolean>>(() => {
const values: string[] = localValue.value || []
return values.reduce((value: Record<string, boolean>, key) => {
value[key] = true
return value
}, {})
})
const parentChildLookup = ref(
props.context.options.reduce(
(lookup: Record<string, PermissionsChildOption[]>, option) => {
lookup[option.value] = option.children
return lookup
},
{},
),
)
const initializeCollapseState = (key: string) =>
localValue.value.some((value: string) =>
parentChildLookup.value[key]?.some((option) => option.value === value),
)
const collapseLookup = ref(
props.context.options.reduce((lookup: Record<string, boolean>, option) => {
lookup[option.value] = initializeCollapseState(option.value)
return lookup
}, {}),
)
const updateValue = (key: string, state: boolean | undefined) => {
const values: string[] = cloneDeep(localValue.value) || []
if (state === true && !values.includes(key)) {
values.push(key)
localValue.value = values
collapseLookup.value[key] = false
} else if (state === false) {
localValue.value = values.filter((value) => value !== key)
collapseLookup.value[key] = initializeCollapseState(key)
}
}
const toggleCollapse = (value: string) => {
collapseLookup.value[value] = !collapseLookup.value[value]
}
const { delegateFocus } = useDelegateFocus(
props.context.id,
`permissions_toggle_${props.context.id}_${props.context?.options && props.context?.options[0]?.value}`,
)
const { collapseDuration, collapseEnter, collapseAfterEnter, collapseLeave } =
useTransitionCollapse()
</script>

<template>
<output
:id="context.id"
class="block rounded-lg bg-blue-200 focus:outline focus:outline-1 focus:outline-offset-1 focus:outline-blue-800 hover:focus:outline-blue-800 dark:bg-gray-700"
role="tree"
:class="context.classes.input"
:name="context.node.name"
:aria-disabled="context.disabled"
:aria-describedby="context.describedBy"
:tabindex="context.disabled ? '-1' : '0'"
v-bind="context.attrs"
@focus="delegateFocus"
>
<div
v-for="(option, index) in context.options"
:key="`option-${option.value}`"
class="flex flex-col"
>
<div
class="flex items-center gap-2.5 px-3 py-2.5"
role="treeitem"
:aria-selected="valueLookup[option.value]"
>
<FormKit
:id="`permissions_toggle_${context.id}_${option.value}`"
:model-value="valueLookup[option.value]"
type="toggle"
:name="`permissions_toggle_${context.id}_${option.value}`"
:ignore="true"
outer-class="grow"
wrapper-class="justify-end gap-2.5 formkit-disabled:opacity-100"
inner-class="formkit-disabled:opacity-50"
:variants="{ true: 'True', false: 'False' }"
:disabled="context.disabled || option.disabled"
size="small"
:label="option.label"
:sections-schema="{
label: {
attrs: {
class: 'flex flex-col cursor-pointer',
for: `permissions_toggle_${context.id}_${option.value}`,
tabindex: '-1',
},
children: [
{
$cmp: 'CommonLabel',
props: {
class: 'text-black dark:text-white',
},
children: i18n.t(option.label, option.value),
},
{
$cmp: 'CommonLabel',
props: {
class: 'text-stone-200 dark:text-neutral-500',
},
children: i18n.t(option.description),
},
],
},
}"
@update:model-value="updateValue(option.value, $event)"
@blur="index === 0 ? context.handlers.blur : undefined"
/>
<CommonIcon
v-if="option.children && !valueLookup[option.value]"
class="shrink-0 fill-stone-200 hover:fill-black focus:outline-none focus-visible:rounded-sm focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-1 focus-visible:outline-blue-800 dark:fill-neutral-500 dark:hover:fill-white"
:aria-label="i18n.t('Toggle Group')"
:name="collapseLookup[option.value] ? 'chevron-up' : 'chevron-down'"
size="xs"
role="button"
tabindex="0"
@click.stop="toggleCollapse(option.value)"
@keypress.enter.prevent.stop="toggleCollapse(option.value)"
@keypress.space.prevent.stop="toggleCollapse(option.value)"
/>
</div>
<Transition
name="collapse"
:duration="collapseDuration"
@enter="collapseEnter"
@after-enter="collapseAfterEnter"
@leave="collapseLeave"
>
<div
v-if="option.children"
v-show="collapseLookup[option.value]"
class="ms-10 flex flex-col"
role="group"
>
<div
v-for="childOption in option.children"
:key="`child-option-${childOption.value}`"
class="flex gap-2.5 px-3 py-2.5"
role="treeitem"
:aria-selected="valueLookup[childOption.value]"
>
<FormKit
:id="`permissions_child_toggle_${context.id}_${childOption.value}`"
:model-value="valueLookup[childOption.value]"
type="toggle"
:name="`permissions_child_toggle_${context.id}_${childOption.value}`"
:ignore="true"
wrapper-class="gap-2.5"
:variants="{ true: 'True', false: 'False' }"
:disabled="context.disabled"
size="small"
:label="childOption.label"
:sections-schema="{
label: {
attrs: {
class: 'flex flex-col cursor-pointer',
for: `permissions_child_toggle_${context.id}_${childOption.value}`,
tabindex: '-1',
},
children: [
{
$cmp: 'CommonLabel',
props: {
class: 'text-black dark:text-white',
},
children: i18n.t(childOption.label, childOption.value),
},
{
$cmp: 'CommonLabel',
props: {
class: 'text-stone-200 dark:text-neutral-500',
},
children: i18n.t(childOption.description),
},
],
},
}"
@update:model-value="updateValue(childOption.value, $event)"
/>
</div>
</div>
</Transition>
</div>
</output>
</template>

0 comments on commit e42f4d9

Please sign in to comment.