Skip to main content
Glama
Collaborators.vue8.56 kB
<template> <div :class=" clsx( 'flex flex-row justify-evenly items-center m-xs', width, moreUsersPopoverRef?.isOpen && 'pointer-events-none', ) " > <!-- Displays all visible users, up to 6 of them --> <template v-if="!(showOneIcon && displayUsers.length > 1)"> <div v-for="(user, index) in displayUsers" :key="index" class="h-8 w-0"> <UserIcon :tooltip="userTooltips[index]" :user="user" class="absolute translate-x-[-50%]" hasHoverState forceDark @click="goToUserChangeSet(user)" /> </div> </template> <!-- If there are 7 or more users or if the screen is small and there are multiple users, some are put into this menu --> <div v-if=" sortedUsers.length !== 1 && (showOneIcon || sortedUsers.length > 6) && sortedUsers.length > 0 " class="h-8 w-0" > <div ref="moreUsersButtonRef" v-tooltip="moreUsersTooltip" :class=" clsx( 'absolute translate-x-[-50%] h-8 w-8 cursor-pointer bg-black', 'border-2 border-shade-0 rounded-full overflow-hidden', 'flex flex-row items-center hover:border-action-300', moreUsersNumber < 10 ? 'text-base' : moreUsersNumber < 100 ? 'text-xs' : 'text-xl', ) " @click="openMoreUsersPopover" > <div class="text-center w-full font-bold"> <Icon v-if="showOneIcon" name="user-circle" size="full" /> <template v-else-if="moreUsersNumber < 100" >+{{ moreUsersNumber }}</template > <template v-else>+</template> </div> </div> </div> <!-- Overflow menu for users --> <Popover ref="moreUsersPopoverRef" popDown :anchorTo="moreUsersButtonRef" onTopOfEverything > <div class="flex flex-col rounded bg-shade-0 dark:bg-shade-100 border dark:border-neutral-500" > <div class="w-full text-center text-xs italic p-xs text-neutral-500 dark:text-neutral-400" > {{ sortedUsers.length }} Users Online </div> <SiSearch class="pt-0" placeholder="search users" @search="onSearchUpdated" /> <div class="flex flex-col max-w-[250px] max-h-[60vh] overflow-x-hidden overflow-y-auto" > <UserCard v-for="(user, index) in filteredUsers" :key="index" :user="user" iconHasHoverState @iconClicked="goToUserChangeSet(user)" /> </div> </div> </Popover> </div> </template> <script lang="ts" setup> import * as _ from "lodash-es"; import { computed, onBeforeUnmount, onMounted, ref } from "vue"; import { SiSearch, Icon } from "@si/vue-lib/design-system"; import clsx from "clsx"; import { useRoute, useRouter } from "vue-router"; import Popover from "@/components/Popover.vue"; import { usePresenceStore } from "@/store/presence.store"; import UserIcon from "./UserIcon.vue"; import UserCard from "./UserCard.vue"; import { useContext } from "../logic_composables/context"; import { useChangeSets } from "../logic_composables/change_set"; const presenceStore = usePresenceStore(); const router = useRouter(); const route = useRoute(); const ctx = useContext(); const { openChangeSets } = useChangeSets(computed(() => ctx)); export type UserInfo = { name: string; color?: string | null; status?: string | null; changeSet?: string; // TODO(Wendy) - this should be called changeSetId pictureUrl?: string | null; view?: string; // TODO(Wendy) - this should be called viewId // TODO(Wendy) - we should probably also send the viewName so we can show it in the tooltip }; const moreUsersPopoverRef = ref(); const moreUsersButtonRef = ref(); // eslint-disable-next-line @typescript-eslint/no-unused-vars const testUsers = (amount = 10) => { return Array.from(Array(amount).keys()).map((i) => ({ name: `test user ${i}`, })); }; const users = computed<UserInfo[]>(() => { // Uncomment this line when you need to see users in the Collaborators widget for testing // return testUsers(); const list = [] as UserInfo[]; for (const user of _.values(presenceStore.usersById)) { list.push({ name: user.name, color: user.color, status: user.idle ? "idle" : "active", changeSet: user.changeSetId, pictureUrl: user.pictureUrl, view: user.viewId, }); } return list; }); const sortedUsers = computed(() => { const usersCopy = _.clone(users.value); return usersCopy.sort((a, b) => { if (ctx.changeSetId.value) { if ( a.changeSet !== ctx.changeSetId.value && b.changeSet === ctx.changeSetId.value ) { return 2; } if ( a.changeSet === ctx.changeSetId.value && b.changeSet !== ctx.changeSetId.value ) { return -2; } } if (a.status === "idle" && b.status !== "idle") return 1; if (a.status !== "idle" && b.status === "idle") return -1; return 0; }); }); const displayUsers = computed(() => { if (sortedUsers.value.length < 7) return sortedUsers.value; else { const displayUsers = sortedUsers.value.slice(0, 5); return displayUsers; } }); const moreUsersNumber = computed(() => { if (showOneIcon.value) return sortedUsers.value.length; else return sortedUsers.value.length - 5; }); const userTooltips = computed(() => { const tooltips = [] as { content: string; theme: string; }[]; displayUsers.value.forEach((user) => { const userChangeSet = openChangeSets.value.find( (changeSet) => changeSet.id === user.changeSet, ); tooltips.push({ content: `<div class='flex flex-col items-center max-w-lg'> <div class='text-center font-bold w-full break-words line-clamp-3 pb-3xs px-sm min-w-0'>${ user.name }</div> <div class='text-xs font-bold w-full text-center line-clamp-3 px-sm'>${ userChangeSet ? userChangeSet.name : "Head" }</div> <div class='text-xs w-full text-center line-clamp-3 px-sm'>${ user.status }</div> </div>`, theme: "user-info", }); }); return tooltips; }); const moreUsersTooltip = computed(() => { let content; if (showOneIcon.value) { content = `<div class="px-xs font-bold">${moreUsersNumber.value} Editors Online</div>`; } else { content = `<div class="px-xs font-bold">${moreUsersNumber.value} More Online</div>`; } return { content, theme: "user-info", }; }); const width = computed(() => { if (showOneIcon.value || sortedUsers.value.length === 1) { return "w-8"; } else if (sortedUsers.value.length < 3) { return "w-12"; } else if (sortedUsers.value.length < 5) { return "w-20"; } else { return "w-28"; } }); const openMoreUsersPopover = () => { moreUsersPopoverRef.value.open(); }; const windowWidth = ref(window.innerWidth); const windowResizeHandler = () => { windowWidth.value = window.innerWidth; }; onMounted(() => { windowResizeHandler(); window.addEventListener("resize", windowResizeHandler); }); onBeforeUnmount(() => { window.removeEventListener("resize", windowResizeHandler); }); const showOneIcon = computed(() => windowWidth.value < 850); const filterString = ref(""); const filterStringCleaned = computed(() => { if (!moreUsersPopoverRef.value.isOpen) return ""; return filterString.value.trim().toLowerCase(); }); function onSearchUpdated(newFilterString: string) { filterString.value = newFilterString; } const filterModeActive = computed(() => !!filterStringCleaned.value); const filteredUsers = computed(() => { if (filterModeActive.value) { return sortedUsers.value.filter((user) => user.name.toLowerCase().includes(filterStringCleaned.value), ); } else return sortedUsers.value; }); function goToUserChangeSet(user: UserInfo) { if (!user || !user.changeSet) return; if (user.view) { router.push({ name: "new-hotness-view", params: { ...route.params, changeSetId: user.changeSet, viewId: user.view, }, query: route.query, }); return; } router.push({ name: "new-hotness", params: { ...route.params, changeSetId: user.changeSet, }, query: route.query, }); } </script>

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/systeminit/si'

If you have feedback or need assistance with the MCP directory API, please join our Discord server