forked from chatwoot/chatwoot
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add new sidebar for Chatwoot V4 (chatwoot#10291)
This PR has the initial version of the new sidebar targeted for the next major redesign of the app. This PR includes the following changes - Components in the `layouts-next` and `base-next` directories in `dashboard/components` - Two generic components `Avatar` and `Icon` - `SidebarGroup` component to manage expandable sidebar groups with nested navigation items. This includes handling active states, transitions, and permissions. - `SidebarGroupHeader` component to display the header of each navigation group with optional icons and active state indication. - `SidebarGroupLeaf` component for individual navigation items within a group, supporting icons and active state. - `SidebarGroupSeparator` component to visually separate nested navigation items. (They look a lot like header) - `SidebarGroupEmptyLeaf` component to render empty state of any navigation groups. ---- Co-authored-by: Pranav <[email protected]> Co-authored-by: Pranav <[email protected]>
- Loading branch information
1 parent
601a0f8
commit 6d3ecfe
Showing
47 changed files
with
2,187 additions
and
154 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
128 changes: 99 additions & 29 deletions
128
app/javascript/dashboard/components-next/avatar/Avatar.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,52 +1,122 @@ | ||
<script setup> | ||
import { computed } from 'vue'; | ||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue'; | ||
import Icon from 'dashboard/components-next/icon/Icon.vue'; | ||
import { computed, ref, watch } from 'vue'; | ||
import wootConstants from 'dashboard/constants/globals'; | ||
const props = defineProps({ | ||
src: { | ||
type: String, | ||
default: '', | ||
}, | ||
name: { | ||
type: String, | ||
required: true, | ||
}, | ||
size: { | ||
type: Number, | ||
default: 72, | ||
default: 32, | ||
}, | ||
allowUpload: { | ||
type: Boolean, | ||
default: false, | ||
}, | ||
roundedFull: { | ||
type: Boolean, | ||
default: false, | ||
}, | ||
status: { | ||
type: String, | ||
default: null, | ||
validator: value => { | ||
if (!value) return true; | ||
return wootConstants.AVAILABILITY_STATUS_KEYS.includes(value); | ||
}, | ||
}, | ||
}); | ||
const emit = defineEmits(['upload']); | ||
const avatarSize = computed(() => `${props.size}px`); | ||
const iconSize = computed(() => `${props.size / 2}px`); | ||
const isImageValid = ref(true); | ||
const handleUploadAvatar = () => { | ||
emit('upload'); | ||
}; | ||
function invalidateCurrentImage() { | ||
isImageValid.value = false; | ||
} | ||
const initials = computed(() => { | ||
const splitNames = props.name.split(' '); | ||
if (splitNames.length > 1) { | ||
const firstName = splitNames[0]; | ||
const lastName = splitNames[splitNames.length - 1]; | ||
return firstName[0] + lastName[0]; | ||
} | ||
const firstName = splitNames[0]; | ||
return firstName[0]; | ||
}); | ||
watch( | ||
() => props.src, | ||
() => { | ||
isImageValid.value = true; | ||
} | ||
); | ||
</script> | ||
|
||
<template> | ||
<div | ||
class="relative flex flex-col items-center gap-2 select-none rounded-xl group/avatar" | ||
<span | ||
class="relative inline" | ||
:style="{ | ||
width: avatarSize, | ||
height: avatarSize, | ||
width: `${size}px`, | ||
height: `${size}px`, | ||
}" | ||
> | ||
<img | ||
v-if="src" | ||
:src="props.src" | ||
alt="avatar" | ||
class="w-full h-full shadow-sm rounded-xl" | ||
/> | ||
<div | ||
class="absolute inset-0 flex items-center justify-center invisible w-full h-full transition-all duration-500 ease-in-out opacity-0 rounded-xl dark:bg-slate-900/50 bg-slate-900/20 group-hover/avatar:visible group-hover/avatar:opacity-100" | ||
@click="handleUploadAvatar" | ||
<slot name="badge" :size> | ||
<div | ||
class="rounded-full w-2.5 h-2.5 absolute z-20" | ||
:style="{ | ||
top: `${size - 10}px`, | ||
left: `${size - 10}px`, | ||
}" | ||
:class="{ | ||
'bg-n-teal-10': status === 'online', | ||
'bg-n-amber-10': status === 'busy', | ||
'bg-n-slate-10': status === 'offline', | ||
}" | ||
/> | ||
</slot> | ||
<span | ||
role="img" | ||
class="inline-flex relative items-center justify-center object-cover overflow-hidden font-medium bg-woot-50 text-woot-500 group/avatar" | ||
:class="{ | ||
'rounded-full': roundedFull, | ||
'rounded-xl': !roundedFull, | ||
}" | ||
:style="{ | ||
width: `${size}px`, | ||
height: `${size}px`, | ||
}" | ||
> | ||
<FluentIcon | ||
icon="upload-lucide" | ||
icon-lib="lucide" | ||
:size="iconSize" | ||
class="text-white dark:text-white" | ||
<img | ||
v-if="src && isImageValid" | ||
:src="src" | ||
:alt="name" | ||
@error="invalidateCurrentImage" | ||
/> | ||
</div> | ||
</div> | ||
<span v-else> | ||
{{ initials }} | ||
</span> | ||
<div | ||
v-if="allowUpload" | ||
role="button" | ||
class="absolute inset-0 flex items-center justify-center invisible w-full h-full transition-all duration-500 ease-in-out opacity-0 rounded-xl dark:bg-slate-900/50 bg-slate-900/20 group-hover/avatar:visible group-hover/avatar:opacity-100" | ||
@click="emit('upload')" | ||
> | ||
<Icon | ||
icon="i-lucide-upload" | ||
class="text-white dark:text-white size-4" | ||
/> | ||
</div> | ||
</span> | ||
</span> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
<script setup> | ||
import { h, isVNode } from 'vue'; | ||
const props = defineProps({ | ||
icon: { type: [String, Object, Function], required: true }, | ||
}); | ||
const renderIcon = () => { | ||
if (!props.icon) return null; | ||
if (typeof props.icon === 'function' || isVNode(props.icon)) { | ||
return props.icon; | ||
} | ||
return h('span', { class: props.icon }); | ||
}; | ||
</script> | ||
|
||
<template> | ||
<component :is="renderIcon" /> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
<template> | ||
<svg | ||
v-once | ||
width="16" | ||
height="16" | ||
viewBox="0 0 16 16" | ||
fill="none" | ||
xmlns="http://www.w3.org/2000/svg" | ||
> | ||
<g clip-path="url(#woot-logo-clip-2342424e23u32098)"> | ||
<path | ||
d="M8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16Z" | ||
fill="#2781F6" | ||
/> | ||
<path | ||
d="M11.4172 11.4172H7.70831C5.66383 11.4172 4 9.75328 4 7.70828C4 5.66394 5.66383 4 7.70835 4C9.75339 4 11.4172 5.66394 11.4172 7.70828V11.4172Z" | ||
fill="white" | ||
stroke="white" | ||
stroke-width="0.1875" | ||
/> | ||
</g> | ||
<defs> | ||
<clipPath id="woot-logo-clip-2342424e23u32098"> | ||
<rect width="16" height="16" fill="white" /> | ||
</clipPath> | ||
</defs> | ||
</svg> | ||
</template> |
71 changes: 71 additions & 0 deletions
71
app/javascript/dashboard/components-next/sidebar/ChannelLeaf.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
<script setup> | ||
import { computed } from 'vue'; | ||
import Icon from 'next/icon/Icon.vue'; | ||
const props = defineProps({ | ||
label: { | ||
type: String, | ||
required: true, | ||
}, | ||
active: { | ||
type: Boolean, | ||
default: false, | ||
}, | ||
inbox: { | ||
type: Object, | ||
required: true, | ||
}, | ||
}); | ||
const channelTypeIconMap = { | ||
'Channel::Api': 'i-ri-cloudy-fill', | ||
'Channel::Email': 'i-ri-mail-fill', | ||
'Channel::FacebookPage': 'i-ri-messenger-fill', | ||
'Channel::Line': 'i-ri-line-fill', | ||
'Channel::Sms': 'i-ri-chat-1-fill', | ||
'Channel::Telegram': 'i-ri-telegram-fill', | ||
'Channel::TwilioSms': 'i-ri-chat-1-fill', | ||
'Channel::TwitterProfile': 'i-ri-twitter-x-fill', | ||
'Channel::WebWidget': 'i-ri-global-fill', | ||
'Channel::Whatsapp': 'i-ri-whatsapp-fill', | ||
}; | ||
const providerIconMap = { | ||
microsoft: 'i-ri-microsoft-fill', | ||
google: 'i-ri-google-fill', | ||
}; | ||
const channelIcon = computed(() => { | ||
const type = props.inbox.channel_type; | ||
let icon = channelTypeIconMap[type]; | ||
if (type === 'Channel::Email' && props.inbox.provider) { | ||
if (Object.keys(providerIconMap).includes(props.inbox.provider)) { | ||
icon = providerIconMap[props.inbox.provider]; | ||
} | ||
} | ||
return icon ?? 'i-ri-global-fill'; | ||
}); | ||
const reauthorizationRequired = computed(() => { | ||
return props.inbox.reauthorization_required; | ||
}); | ||
</script> | ||
<template> | ||
<span | ||
class="size-4 grid place-content-center rounded-full bg-n-alpha-2" | ||
:class="{ 'bg-n-blue/20': active }" | ||
> | ||
<Icon :icon="channelIcon" class="size-3" /> | ||
</span> | ||
<div class="flex-1 truncate min-w-0">{{ label }}</div> | ||
<div | ||
v-if="reauthorizationRequired" | ||
v-tooltip.top-end="$t('SIDEBAR.REAUTHORIZE')" | ||
class="grid place-content-center size-5 bg-n-ruby-5/60 rounded-full" | ||
> | ||
<Icon icon="i-woot-alert" class="size-3 text-n-ruby-9" /> | ||
</div> | ||
</template> |
Oops, something went wrong.