Loading backend @ c359c0e1 Compare f5e9b850 to c359c0e1 Original line number Diff line number Diff line Subproject commit f5e9b85060de29b52877c306fafcd8d9fc810c1b Subproject commit c359c0e1adf356048a6da9d9052c4460861d8be2 editor/src/components/Select/GroupedSelect.tsx +1 −1 Original line number Diff line number Diff line Loading @@ -17,7 +17,7 @@ export interface OptionWithNoGroup { label: string value: string | number } const dropdownStyles = css` export const dropdownStyles = css` .bp5-menu { max-height: 450px; overflow-y: auto; Loading frontend/src/pages/(navbar)/settings.tsx +13 −2 Original line number Diff line number Diff line Loading @@ -5,6 +5,7 @@ import Experimental from '@/clientsettings/components/Experimental' import NotificationSystem from '@/clientsettings/components/NotificactionSystem' import NotificationLimit from '@/clientsettings/components/NotificationLimit' import { useExperimentalV2 } from '@/clientsettings/vars/experimentalV2' import Tags from '@/components/Tags' import UserDetailRow from '@/users/UserDetail/UserDetailRow' import { Section, SectionCard } from '@blueprintjs/core' import { css } from '@emotion/css' Loading @@ -28,6 +29,14 @@ const sectionBody = css` flex-direction: column; gap: 1rem; ` const sectionCard = css` display: grid; grid-template-columns: 1fr 5fr; column-gap: 1rem; row-gap: 0.5rem; align-items: center; max-width: max-content; ` export const Pending = () => <CenteredSpinner /> Loading @@ -41,7 +50,7 @@ const Settings = () => { <Section title='Settings' icon='cog'> <SectionCard padded title='User'> <Section title={whoAmI?.username} icon='user'> <SectionCard padded> <SectionCard padded className={sectionCard}> {whoAmI?.dateJoined && ( <UserDetailRow name='Date joined' Loading @@ -57,7 +66,9 @@ const Settings = () => { {whoAmI?.tags && ( <UserDetailRow name='Tags' value={(whoAmI?.tags.map(tag => tag.name) || []).join(', ')} value={ <Tags tags={whoAmI?.tags.map(tag => tag.name) || []} /> } /> )} </SectionCard> Loading frontend/src/pages/(navbar)/users/index.tsx +22 −5 Original line number Diff line number Diff line Loading @@ -4,6 +4,8 @@ import Sidebar from '@/components/Sidebar' import Table from '@/components/Table' import { useNavigate } from '@/router' import RemoveUsers from '@/users/RemoveUsers' import AssignTags from '@/users/Tags/AssignTags' import TagManager from '@/users/Tags/TagManager' import UserCreator from '@/users/UserCreator' import Active from '@/users/UserTable/Filters/Active' import Columns from '@/users/UserTable/Filters/Columns' Loading @@ -21,10 +23,11 @@ import { Button, ButtonGroup, InputGroup, Navbar } from '@blueprintjs/core' import { css, cx } from '@emotion/css' import type { User } from '@inject/graphql' import { GetUsers, Reloader, ReloadTable, useAuthIdentity, useClient, useTypedQuery, } from '@inject/graphql' import { responsiveButtonGroup, useSetPageTitle } from '@inject/shared' import { useState } from 'react' Loading Loading @@ -105,17 +108,18 @@ const Page = () => { }) const [tags, setTags] = useState<string[]>([]) const [columns, setColumns] = useState<UserColumn[]>(USER_COLUMNS) const { id: currentId } = useAuthIdentity() const [selectedUsers, setSelectedUsers] = useState<string[]>([]) const [{ data }] = useTypedQuery({ query: GetUsers, }) const tableProps = useUserTable({ onClick: (id: string) => { nav('/users/:userId', { params: { userId: id } }) }, onCheckboxClick: (id: string) => { if (currentId !== id) { SelectUserHandler({ selectedUsers, setSelectedUsers, id }) } }, groups, selectedUsers, Loading Loading @@ -168,6 +172,19 @@ const Page = () => { </> ), }, { id: 'tags', name: 'Tags', node: ( <> <AssignTags selectedUsers={selectedUsers} setSearchString={setSearchString} /> <TagManager allUsers={data?.users || []} /> </> ), }, ] const element = usePrependNavbar( Loading frontend/src/users/Tags/AssignTags.tsx 0 → 100644 +133 −0 Original line number Diff line number Diff line import { Button, Popover } from '@blueprintjs/core' import { css } from '@emotion/css' import { ReloadTable, UpdateTagAssignments, useClient, useTypedMutation, type CustomOperationContext, type Tag, } from '@inject/graphql' import { ErrorMessage } from '@inject/shared' import { useCallback, useState } from 'react' import TagSelect from '../UserCreator/TagSelect' const contentWrapper = css` min-width: 18rem; max-height: 20rem; padding: 1rem; overflow-y: auto; max-width: 25rem; z-index: 1; ` const buttonWrapper = css` padding-top: 0.5rem; display: flex; flex-direction: column; gap: 0.5rem; ` type Props = { selectedUsers: string[] setSearchString: React.Dispatch<React.SetStateAction<string>> } const AssignTags = ({ selectedUsers, setSearchString }: Props) => { const [open, setOpen] = useState(false) const [tags, setTags] = useState<Tag[]>([]) const [error, setError] = useState('') const client = useClient() const [{ fetching: loading }, assingTagsMutation] = useTypedMutation(UpdateTagAssignments) const updateTagAssignment = useCallback( (assign: boolean) => { let addTags: string[] = [] let removeTags: string[] = [] if (assign) { addTags = tags.map(tag => tag.id) } else { removeTags = tags.map(tag => tag.id) } assingTagsMutation( { userIds: selectedUsers, addTags, removeTags, }, { errorHandled: true } as CustomOperationContext ).then(result => { if (result.error) { setError(result.error?.message || 'Something went wrong') return } client.mutation(ReloadTable, {}).toPromise() setTags([]) setSearchString('') }) }, [assingTagsMutation, client, selectedUsers, setSearchString, tags] ) return ( <> <Popover fill minimal position={'left'} autoFocus={false} enforceFocus={false} onClose={() => setOpen(false)} content={ <div> <div className={contentWrapper}> <TagSelect setTags={setTags} tags={tags} /> <div className={buttonWrapper}> <Button loading={loading} intent={'primary'} disabled={selectedUsers.length === 0 || tags.length === 0} onClick={() => updateTagAssignment(true)} icon='add' alignText='left' > Assign to selected ({selectedUsers.length}) </Button> <Button loading={loading} intent={'danger'} disabled={selectedUsers.length === 0 || tags.length === 0} onClick={() => updateTagAssignment(false)} icon='remove' alignText='left' > Remove from selected ({selectedUsers.length}) </Button> <ErrorMessage minimal>{error}</ErrorMessage> </div> </div> </div> } isOpen={open} > <Button title={'Assign tags'} active={open} icon={'swap-horizontal'} alignText='left' fill minimal onClick={() => { setOpen(prev => !prev) setTags([]) }} > Tag assignments </Button> </Popover> </> ) } export default AssignTags Loading
backend @ c359c0e1 Compare f5e9b850 to c359c0e1 Original line number Diff line number Diff line Subproject commit f5e9b85060de29b52877c306fafcd8d9fc810c1b Subproject commit c359c0e1adf356048a6da9d9052c4460861d8be2
editor/src/components/Select/GroupedSelect.tsx +1 −1 Original line number Diff line number Diff line Loading @@ -17,7 +17,7 @@ export interface OptionWithNoGroup { label: string value: string | number } const dropdownStyles = css` export const dropdownStyles = css` .bp5-menu { max-height: 450px; overflow-y: auto; Loading
frontend/src/pages/(navbar)/settings.tsx +13 −2 Original line number Diff line number Diff line Loading @@ -5,6 +5,7 @@ import Experimental from '@/clientsettings/components/Experimental' import NotificationSystem from '@/clientsettings/components/NotificactionSystem' import NotificationLimit from '@/clientsettings/components/NotificationLimit' import { useExperimentalV2 } from '@/clientsettings/vars/experimentalV2' import Tags from '@/components/Tags' import UserDetailRow from '@/users/UserDetail/UserDetailRow' import { Section, SectionCard } from '@blueprintjs/core' import { css } from '@emotion/css' Loading @@ -28,6 +29,14 @@ const sectionBody = css` flex-direction: column; gap: 1rem; ` const sectionCard = css` display: grid; grid-template-columns: 1fr 5fr; column-gap: 1rem; row-gap: 0.5rem; align-items: center; max-width: max-content; ` export const Pending = () => <CenteredSpinner /> Loading @@ -41,7 +50,7 @@ const Settings = () => { <Section title='Settings' icon='cog'> <SectionCard padded title='User'> <Section title={whoAmI?.username} icon='user'> <SectionCard padded> <SectionCard padded className={sectionCard}> {whoAmI?.dateJoined && ( <UserDetailRow name='Date joined' Loading @@ -57,7 +66,9 @@ const Settings = () => { {whoAmI?.tags && ( <UserDetailRow name='Tags' value={(whoAmI?.tags.map(tag => tag.name) || []).join(', ')} value={ <Tags tags={whoAmI?.tags.map(tag => tag.name) || []} /> } /> )} </SectionCard> Loading
frontend/src/pages/(navbar)/users/index.tsx +22 −5 Original line number Diff line number Diff line Loading @@ -4,6 +4,8 @@ import Sidebar from '@/components/Sidebar' import Table from '@/components/Table' import { useNavigate } from '@/router' import RemoveUsers from '@/users/RemoveUsers' import AssignTags from '@/users/Tags/AssignTags' import TagManager from '@/users/Tags/TagManager' import UserCreator from '@/users/UserCreator' import Active from '@/users/UserTable/Filters/Active' import Columns from '@/users/UserTable/Filters/Columns' Loading @@ -21,10 +23,11 @@ import { Button, ButtonGroup, InputGroup, Navbar } from '@blueprintjs/core' import { css, cx } from '@emotion/css' import type { User } from '@inject/graphql' import { GetUsers, Reloader, ReloadTable, useAuthIdentity, useClient, useTypedQuery, } from '@inject/graphql' import { responsiveButtonGroup, useSetPageTitle } from '@inject/shared' import { useState } from 'react' Loading Loading @@ -105,17 +108,18 @@ const Page = () => { }) const [tags, setTags] = useState<string[]>([]) const [columns, setColumns] = useState<UserColumn[]>(USER_COLUMNS) const { id: currentId } = useAuthIdentity() const [selectedUsers, setSelectedUsers] = useState<string[]>([]) const [{ data }] = useTypedQuery({ query: GetUsers, }) const tableProps = useUserTable({ onClick: (id: string) => { nav('/users/:userId', { params: { userId: id } }) }, onCheckboxClick: (id: string) => { if (currentId !== id) { SelectUserHandler({ selectedUsers, setSelectedUsers, id }) } }, groups, selectedUsers, Loading Loading @@ -168,6 +172,19 @@ const Page = () => { </> ), }, { id: 'tags', name: 'Tags', node: ( <> <AssignTags selectedUsers={selectedUsers} setSearchString={setSearchString} /> <TagManager allUsers={data?.users || []} /> </> ), }, ] const element = usePrependNavbar( Loading
frontend/src/users/Tags/AssignTags.tsx 0 → 100644 +133 −0 Original line number Diff line number Diff line import { Button, Popover } from '@blueprintjs/core' import { css } from '@emotion/css' import { ReloadTable, UpdateTagAssignments, useClient, useTypedMutation, type CustomOperationContext, type Tag, } from '@inject/graphql' import { ErrorMessage } from '@inject/shared' import { useCallback, useState } from 'react' import TagSelect from '../UserCreator/TagSelect' const contentWrapper = css` min-width: 18rem; max-height: 20rem; padding: 1rem; overflow-y: auto; max-width: 25rem; z-index: 1; ` const buttonWrapper = css` padding-top: 0.5rem; display: flex; flex-direction: column; gap: 0.5rem; ` type Props = { selectedUsers: string[] setSearchString: React.Dispatch<React.SetStateAction<string>> } const AssignTags = ({ selectedUsers, setSearchString }: Props) => { const [open, setOpen] = useState(false) const [tags, setTags] = useState<Tag[]>([]) const [error, setError] = useState('') const client = useClient() const [{ fetching: loading }, assingTagsMutation] = useTypedMutation(UpdateTagAssignments) const updateTagAssignment = useCallback( (assign: boolean) => { let addTags: string[] = [] let removeTags: string[] = [] if (assign) { addTags = tags.map(tag => tag.id) } else { removeTags = tags.map(tag => tag.id) } assingTagsMutation( { userIds: selectedUsers, addTags, removeTags, }, { errorHandled: true } as CustomOperationContext ).then(result => { if (result.error) { setError(result.error?.message || 'Something went wrong') return } client.mutation(ReloadTable, {}).toPromise() setTags([]) setSearchString('') }) }, [assingTagsMutation, client, selectedUsers, setSearchString, tags] ) return ( <> <Popover fill minimal position={'left'} autoFocus={false} enforceFocus={false} onClose={() => setOpen(false)} content={ <div> <div className={contentWrapper}> <TagSelect setTags={setTags} tags={tags} /> <div className={buttonWrapper}> <Button loading={loading} intent={'primary'} disabled={selectedUsers.length === 0 || tags.length === 0} onClick={() => updateTagAssignment(true)} icon='add' alignText='left' > Assign to selected ({selectedUsers.length}) </Button> <Button loading={loading} intent={'danger'} disabled={selectedUsers.length === 0 || tags.length === 0} onClick={() => updateTagAssignment(false)} icon='remove' alignText='left' > Remove from selected ({selectedUsers.length}) </Button> <ErrorMessage minimal>{error}</ErrorMessage> </div> </div> </div> } isOpen={open} > <Button title={'Assign tags'} active={open} icon={'swap-horizontal'} alignText='left' fill minimal onClick={() => { setOpen(prev => !prev) setTags([]) }} > Tag assignments </Button> </Popover> </> ) } export default AssignTags