diff --git a/client/components/FormControl/ProjectPickerControl/ProjectPickerControl.module.scss b/client/components/FormControl/ProjectPickerControl/ProjectPickerControl.module.scss new file mode 100644 index 000000000..6723dcceb --- /dev/null +++ b/client/components/FormControl/ProjectPickerControl/ProjectPickerControl.module.scss @@ -0,0 +1,22 @@ +.projectPickerControl { + position: inherit; + + .selectedLabels { + display: flex; + flex-direction: row; + flex-wrap: wrap; + + >div { + margin-bottom: 5px; + } + } + + .openPickerButton { + margin: 12px 0; + } + + .noneSelected { + font-size: 12px; + color: #586069; + } +} diff --git a/client/components/FormControl/ProjectPickerControl/ProjectPickerControl.tsx b/client/components/FormControl/ProjectPickerControl/ProjectPickerControl.tsx new file mode 100644 index 000000000..424be5e10 --- /dev/null +++ b/client/components/FormControl/ProjectPickerControl/ProjectPickerControl.tsx @@ -0,0 +1,31 @@ +import { SearchProject } from 'components/SearchProject' +import React from 'react' +import { FormInputControlComponent } from '../types' +import styles from './ProjectPickerControl.module.scss' +import { IProjectPickerControlProps } from './types' +import { useProjectPickerControl } from './useProjectPickerControl' + +/** + * @category Reusable Component + */ +export const ProjectPickerControl: FormInputControlComponent< + IProjectPickerControlProps +> = (props) => { + const { onSelected } = useProjectPickerControl(props) + return ( + + project?.customer?.key === props.model.value('customerKey') + } + onSelected={onSelected} + selectedKey={props.model.value(props.name)} + /> + ) +} + +ProjectPickerControl.displayName = 'ProjectPickerControl' +ProjectPickerControl.className = styles.projectPickerControl +ProjectPickerControl.defaultProps = {} diff --git a/client/components/FormControl/ProjectPickerControl/index.ts b/client/components/FormControl/ProjectPickerControl/index.ts new file mode 100644 index 000000000..08e6edb5e --- /dev/null +++ b/client/components/FormControl/ProjectPickerControl/index.ts @@ -0,0 +1,2 @@ +export * from './ProjectPickerControl' +export * from './types' diff --git a/client/components/FormControl/ProjectPickerControl/types.ts b/client/components/FormControl/ProjectPickerControl/types.ts new file mode 100644 index 000000000..ca0c56d96 --- /dev/null +++ b/client/components/FormControl/ProjectPickerControl/types.ts @@ -0,0 +1,8 @@ +import { ISearchProjectProps } from 'components/SearchProject' +import { HTMLAttributes } from 'react' +import { FormInputControlBase } from '../types' + +export interface IProjectPickerControlProps + extends FormInputControlBase, + Pick, + Omit, 'onChange'> {} diff --git a/client/components/FormControl/ProjectPickerControl/useProjectPickerControl.tsx b/client/components/FormControl/ProjectPickerControl/useProjectPickerControl.tsx new file mode 100644 index 000000000..2089edad1 --- /dev/null +++ b/client/components/FormControl/ProjectPickerControl/useProjectPickerControl.tsx @@ -0,0 +1,14 @@ +import { ISearchProjectProps } from 'components/SearchProject/types' +import { IProjectPickerControlProps } from './types' + +/** + * Hook for the `ProjectPickerControl` component. + * + * @param props Props for the `ProjectPickerControl` component. + */ +export function useProjectPickerControl(props: IProjectPickerControlProps) { + const onSelected: ISearchProjectProps['onSelected'] = (project) => { + props.model.set(props.name, project.tag) + } + return { onSelected } +} diff --git a/client/components/FormControl/index.ts b/client/components/FormControl/index.ts index ddcb047ed..1732ad0ff 100644 --- a/client/components/FormControl/index.ts +++ b/client/components/FormControl/index.ts @@ -16,6 +16,7 @@ export * from './ListControl' export * from './SwitchControl' export * from './SliderControl' export * from './DateControl' +export * from './ProjectPickerControl' export * from './types' export * from './useFormControlModel' export * from './useFormControls' diff --git a/client/components/ProjectLink/ProjectLink.tsx b/client/components/ProjectLink/ProjectLink.tsx index 49be0e3c3..95e8b70dc 100644 --- a/client/components/ProjectLink/ProjectLink.tsx +++ b/client/components/ProjectLink/ProjectLink.tsx @@ -31,7 +31,11 @@ export const ProjectLink: ReusableComponent = (props) => { props.onClick && props.onClick(null)} + onClick={() => { + if (props.onClick) { + props.onClick(null) + } + }} > {props.text ?? props.project?.name} diff --git a/client/components/SearchProject/SearchProject.tsx b/client/components/SearchProject/SearchProject.tsx index b2db6c544..dc14a603f 100644 --- a/client/components/SearchProject/SearchProject.tsx +++ b/client/components/SearchProject/SearchProject.tsx @@ -12,15 +12,19 @@ import { useSearchProject } from './useSearchProject' export const SearchProject: ReusableComponent = ( props ) => { - const [items, disabled] = useSearchProject() + const [items, disabled] = useSearchProject(props) return ( props.onSelected(item?.data)} autoFocus={props.autoFocus} /> ) } + +SearchProject.displayName = 'SearchProject' +SearchProject.defaultProps = { + filterFunc: () => true +} diff --git a/client/components/SearchProject/types.ts b/client/components/SearchProject/types.ts index 7eaeab6f1..dcfa7d9bd 100644 --- a/client/components/SearchProject/types.ts +++ b/client/components/SearchProject/types.ts @@ -6,7 +6,12 @@ export interface ISearchProjectProps extends ISearchBoxProps, Pick< IAutocompleteControlProps, - 'initialFilter' | 'intialFilterPlaceholder' + | 'initialFilter' + | 'intialFilterPlaceholder' + | 'label' + | 'placeholder' + | 'description' + | 'selectedKey' > { /** * Callback when a project is selected. @@ -14,4 +19,12 @@ export interface ISearchProjectProps * @param project The selected project */ onSelected: (project: Project) => void + + /** + * Optional filter function to apply to limit + * the projects that are displayed. + * + * @param project Project to filter + */ + filterFunc?: (project?: Project) => boolean } diff --git a/client/components/SearchProject/useSearchProject.ts b/client/components/SearchProject/useSearchProject.ts index ce5d745df..927866cc1 100644 --- a/client/components/SearchProject/useSearchProject.ts +++ b/client/components/SearchProject/useSearchProject.ts @@ -4,19 +4,24 @@ import { Project } from 'types' import { arrayMap } from 'utils' import { ISuggestionItem } from '../FormControl/AutocompleteControl' import $projects from './projects.gql' +import { ISearchProjectProps } from './types' /** * Component logic hook for ``. Handles * fetching of projects using the query in `projects.gql`, * removes inactive projects and maps the result to an * array of `ISuggestionItem`. + * + * @param props The props for the `` component */ -export function useSearchProject() { +export function useSearchProject(props: ISearchProjectProps) { const { data, loading } = useQuery<{ projects: Project[] }>($projects, { fetchPolicy: 'cache-and-network' }) - const projects = data?.projects.filter((project) => !project.inactive) + const projects = data?.projects.filter( + (project) => !project.inactive && props.filterFunc(project) + ) const items: ISuggestionItem[] = useMemo( () => diff --git a/client/i18n/en-GB.json b/client/i18n/en-GB.json index 6779db65d..35e3376ba 100644 --- a/client/i18n/en-GB.json +++ b/client/i18n/en-GB.json @@ -152,7 +152,11 @@ }, "resourcesLabel": "Project Resources", "hourlyRate": "kr {{rate}} / hour", - "roleFieldLabel": "Project Role" + "roleFieldLabel": "Project Role", + "parentProject": "Parent Project", + "parentProjectDescription": "Select a parent project to which this project belongs. This is particularly useful for organizing projects hierarchically and maintaining connections between related projects.", + "parentLabel": "Parent Project", + "childrenLabel": "Child Projects" }, "customers": { "inactiveText": "This customer has been marked as inactive.", diff --git a/client/i18n/nb.json b/client/i18n/nb.json index a35259827..cfda0093e 100644 --- a/client/i18n/nb.json +++ b/client/i18n/nb.json @@ -150,7 +150,11 @@ }, "resourcesLabel": "Prosjektressurser", "hourlyRate": "kr {{rate}} / time", - "roleFieldLabel": "Prosjektrolle" + "roleFieldLabel": "Prosjektrolle", + "parentProject": "Overordnet prosjekt", + "parentProjectDescription": "Velg et overordnet prosjekt som dette prosjektet skal tilhøre. Dette er spesielt nyttig for å organisere prosjekter hierarkisk og opprettholde sammenhenger mellom relaterte prosjekter.", + "parentLabel": "Overordnet prosjekt", + "childrenLabel": "Underordnede prosjekter" }, "customers": { "inactiveText": "Denne kunden er markert som inaktiv.", diff --git a/client/i18n/nn.json b/client/i18n/nn.json index 5e798ee4c..d105fe4d1 100644 --- a/client/i18n/nn.json +++ b/client/i18n/nn.json @@ -153,7 +153,11 @@ }, "resourcesLabel": "Prosjektressurser", "hourlyRate": "kr {{rate}} / time", - "roleFieldLabel": "Prosjektrolle" + "roleFieldLabel": "Prosjektrolle", + "parentProject": "Overordna prosjekt", + "parentProjectDescription": "Vel eit overordna prosjekt som dette prosjektet høyrer til. Dette er særleg nyttig for å organisere prosjekt hierarkisk og oppretthalde samanhengar mellom relaterte prosjekt.", + "parentLabel": "Overordna prosjekt", + "childrenLabel": "Underordna prosjekter" }, "customers": { "inactiveText": "Denne kunden er markert som inaktiv.", diff --git a/client/pages/Projects/ProjectDetails/ProjectInformation/ProjectInformation.tsx b/client/pages/Projects/ProjectDetails/ProjectInformation/ProjectInformation.tsx index d35b02d25..364ebf6f7 100644 --- a/client/pages/Projects/ProjectDetails/ProjectInformation/ProjectInformation.tsx +++ b/client/pages/Projects/ProjectDetails/ProjectInformation/ProjectInformation.tsx @@ -1,6 +1,7 @@ import { EntityLabel, InformationProperty, + ProjectLink, ProjectTag, UserMessage } from 'components' @@ -13,6 +14,7 @@ import styles from './ProjectInformation.module.scss' import { BudgetTracking } from './BudgetTracking' import ReactMarkdown from 'react-markdown' import { ProjectResources } from './ProjectResources' +import { SET_SELECTED_PROJECT } from 'pages/Projects/reducer' /** * Shows details about the selected project. @@ -59,6 +61,39 @@ export const ProjectInformation: StyledComponent = () => { ))} +