Commit 741b17dd authored by Marek Veselý's avatar Marek Veselý
Browse files

Merge branch '432-implement-tag-management' into 'main'

Feat: Implemented tag managment

Closes #432

See merge request inject/frontend!571
parents 40f4ccc9 c7c9a5c4
Loading
Loading
Loading
Loading
Compare f5e9b850 to c359c0e1
Original line number Diff line number Diff line
Subproject commit f5e9b85060de29b52877c306fafcd8d9fc810c1b
Subproject commit c359c0e1adf356048a6da9d9052c4460861d8be2
+1 −1
Original line number Diff line number Diff line
@@ -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;
+13 −2
Original line number Diff line number Diff line
@@ -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'
@@ -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 />

@@ -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'
@@ -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>
+22 −5
Original line number Diff line number Diff line
@@ -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'
@@ -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'
@@ -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,
@@ -168,6 +172,19 @@ const Page = () => {
        </>
      ),
    },
    {
      id: 'tags',
      name: 'Tags',
      node: (
        <>
          <AssignTags
            selectedUsers={selectedUsers}
            setSearchString={setSearchString}
          />
          <TagManager allUsers={data?.users || []} />
        </>
      ),
    },
  ]

  const element = usePrependNavbar(
+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