diff --git a/app/components/selections/FrameworkTagSelectInput/index.tsx b/app/components/selections/FrameworkTagSelectInput/index.tsx new file mode 100644 index 0000000000..5159a095ba --- /dev/null +++ b/app/components/selections/FrameworkTagSelectInput/index.tsx @@ -0,0 +1,145 @@ +import React, { useMemo, useCallback } from 'react'; +import { + Button, + BadgeInput, + BadgeInputProps, +} from '@the-deep/deep-ui'; +import { useQuery, gql } from '@apollo/client'; + +import { + FrameworkTagOptionsQuery, + FrameworkTagOptionsQueryVariables, +} from '#generated/types'; + +import styles from './styles.css'; + +const FRAMEWORK_TAGS = gql` + query FrameworkTagOptions( + $page: Int, + $pageSize: Int, + ) { + analysisFrameworkTags( + page: $page, + pageSize: $pageSize, + ) { + page + pageSize + results { + description + id + title + icon { + name + url + } + } + totalCount + } + } +`; + +const PAGE_SIZE = 10; + +type BasicFrameworkTag = NonNullable['analysisFrameworkTags']>['results']>[number]; +const keySelector = (item: BasicFrameworkTag) => item.id; +const labelSelector = (item: BasicFrameworkTag) => item.title; +const titleSelector = (item: BasicFrameworkTag) => item.description; +function iconSelector(item: BasicFrameworkTag) { + if (!item.icon?.url) { + return undefined; + } + return ( + {item.icon.url} + ); +} + +type Props = Omit< + BadgeInputProps, + 'options' | 'keySelector' | 'labelSelector' +>; + +function FrameworkTagSelectInput( + props: Props, +) { + const variables = useMemo(() => ({ + page: 1, + pageSize: PAGE_SIZE, + }), []); + + const { + data, + fetchMore, + } = useQuery( + FRAMEWORK_TAGS, + { + variables, + }, + ); + + const handleShowMoreClick = useCallback(() => { + fetchMore({ + variables: { + ...variables, + page: (data?.analysisFrameworkTags?.page ?? 1) + 1, + }, + updateQuery: (previousResult, { fetchMoreResult }) => { + if (!previousResult.analysisFrameworkTags) { + return previousResult; + } + + const oldFrameworkTags = previousResult.analysisFrameworkTags; + const newFrameworkTags = fetchMoreResult?.analysisFrameworkTags; + + if (!newFrameworkTags) { + return previousResult; + } + + return ({ + ...previousResult, + analysisFrameworkTags: { + ...newFrameworkTags, + results: [ + ...(oldFrameworkTags.results ?? []), + ...(newFrameworkTags.results ?? []), + ], + }, + }); + }, + }); + }, [ + data?.analysisFrameworkTags?.page, + fetchMore, + variables, + ]); + + return ( + <> + + {(data?.analysisFrameworkTags?.totalCount ?? 0) + > (data?.analysisFrameworkTags?.results ?? []).length && ( + + )} + + ); +} + +export default FrameworkTagSelectInput; diff --git a/app/components/selections/FrameworkTagSelectInput/styles.css b/app/components/selections/FrameworkTagSelectInput/styles.css new file mode 100644 index 0000000000..965ca45ff6 --- /dev/null +++ b/app/components/selections/FrameworkTagSelectInput/styles.css @@ -0,0 +1,5 @@ +.icon { + width: auto; + height: 1rem; + object-fit: contain; +} diff --git a/app/views/ProjectEdit/Framework/index.tsx b/app/views/ProjectEdit/Framework/index.tsx index 46be87e2e2..6b06d35f49 100644 --- a/app/views/ProjectEdit/Framework/index.tsx +++ b/app/views/ProjectEdit/Framework/index.tsx @@ -8,6 +8,7 @@ import { import { Button, Container, + Checkbox, Kraken, ListView, Message, @@ -29,6 +30,7 @@ import { import SmartButtonLikeLink from '#base/components/SmartButtonLikeLink'; import useDebouncedValue from '#hooks/useDebouncedValue'; import ProjectContext from '#base/context/ProjectContext'; +import FrameworkTagSelectInput from '#components/selections/FrameworkTagSelectInput'; import { isFiltered } from '#utils/common'; import routes from '#base/configs/routes'; import _ts from '#ts'; @@ -49,7 +51,9 @@ const frameworkKeySelector = (d: FrameworkType) => d.id; export const PROJECT_FRAMEWORKS = gql` query ProjectAnalysisFrameworks( $isCurrentUserMember: Boolean, + $tags: [ID!], $search: String, + $recentlyUsed: Boolean, $page: Int, $pageSize: Int, $createdBy: [ID!], @@ -57,8 +61,10 @@ export const PROJECT_FRAMEWORKS = gql` analysisFrameworks( search: $search, isCurrentUserMember: $isCurrentUserMember + recentlyUsed: $recentlyUsed, page: $page, pageSize: $pageSize, + tags: $tags, createdBy: $createdBy, ) { results { @@ -133,7 +139,9 @@ const relatedToMeLabelSelector = (d: Option) => d.label; type FormType = { relatedToMe?: 'true' | 'false'; + recentlyUsed: boolean; search: string; + tag?: string; }; type FormSchema = ObjectSchema>; @@ -142,12 +150,16 @@ type FormSchemaFields = ReturnType const schema: FormSchema = { fields: (): FormSchemaFields => ({ search: [], + tag: [], + recentlyUsed: [], relatedToMe: [requiredCondition], }), }; const defaultFormValue: PartialForm = { relatedToMe: 'true', + recentlyUsed: false, + tag: undefined, search: '', }; @@ -183,12 +195,16 @@ function ProjectFramework(props: Props) { const analysisFrameworkVariables = useMemo(() => ( { isCurrentUserMember: delayedValue.relatedToMe === 'true' ? true : undefined, + tags: delayedValue.tag ? [delayedValue.tag] : undefined, + recentlyUsed: delayedValue.recentlyUsed, search: delayedValue.search, page: 1, pageSize: PAGE_SIZE, } ), [ delayedValue.relatedToMe, + delayedValue.recentlyUsed, + delayedValue.tag, delayedValue.search, ]); @@ -287,6 +303,17 @@ function ProjectFramework(props: Props) { value={value.search} placeholder={_ts('projectEdit', 'searchLabel')} /> + +