Skip to content

Commit

Permalink
Support for parent project
Browse files Browse the repository at this point in the history
  • Loading branch information
olemp committed Jan 23, 2025
1 parent 72389af commit 86deb49
Show file tree
Hide file tree
Showing 22 changed files with 248 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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 (
<SearchProject
label={props.label}
description={props.description}
placeholder={props.placeholder}
filterFunc={(project) =>
project?.customer?.key === props.model.value('customerKey')
}
onSelected={onSelected}
selectedKey={props.model.value(props.name)}
/>
)
}

ProjectPickerControl.displayName = 'ProjectPickerControl'
ProjectPickerControl.className = styles.projectPickerControl
ProjectPickerControl.defaultProps = {}
2 changes: 2 additions & 0 deletions client/components/FormControl/ProjectPickerControl/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './ProjectPickerControl'
export * from './types'
8 changes: 8 additions & 0 deletions client/components/FormControl/ProjectPickerControl/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ISearchProjectProps } from 'components/SearchProject'
import { HTMLAttributes } from 'react'
import { FormInputControlBase } from '../types'

export interface IProjectPickerControlProps
extends FormInputControlBase,
Pick<ISearchProjectProps, 'label' | 'placeholder' | 'description'>,
Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {}
Original file line number Diff line number Diff line change
@@ -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 }
}
1 change: 1 addition & 0 deletions client/components/FormControl/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
6 changes: 5 additions & 1 deletion client/components/ProjectLink/ProjectLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ export const ProjectLink: ReusableComponent<IProjectLinkProps> = (props) => {
<Link
className={styles.link}
to={to}
onClick={() => props.onClick && props.onClick(null)}
onClick={() => {
if (props.onClick) {
props.onClick(null)
}
}}
>
<span>{props.text ?? props.project?.name}</span>
</Link>
Expand Down
8 changes: 6 additions & 2 deletions client/components/SearchProject/SearchProject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,19 @@ import { useSearchProject } from './useSearchProject'
export const SearchProject: ReusableComponent<ISearchProjectProps> = (
props
) => {
const [items, disabled] = useSearchProject()
const [items, disabled] = useSearchProject(props)
return (
<AutocompleteControl
{...props}
disabled={disabled}
items={items}
placeholder={props.placeholder}
onSelected={(item) => props.onSelected(item?.data)}
autoFocus={props.autoFocus}
/>
)
}

SearchProject.displayName = 'SearchProject'
SearchProject.defaultProps = {
filterFunc: () => true
}
15 changes: 14 additions & 1 deletion client/components/SearchProject/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,25 @@ export interface ISearchProjectProps
extends ISearchBoxProps,
Pick<
IAutocompleteControlProps,
'initialFilter' | 'intialFilterPlaceholder'
| 'initialFilter'
| 'intialFilterPlaceholder'
| 'label'
| 'placeholder'
| 'description'
| 'selectedKey'
> {
/**
* Callback when a project is selected.
*
* @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
}
9 changes: 7 additions & 2 deletions client/components/SearchProject/useSearchProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<SearchProject />`. Handles
* fetching of projects using the query in `projects.gql`,
* removes inactive projects and maps the result to an
* array of `ISuggestionItem<Project>`.
*
* @param props The props for the `<SearchProject />` 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<Project>[] = useMemo(
() =>
Expand Down
6 changes: 5 additions & 1 deletion client/i18n/en-GB.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
6 changes: 5 additions & 1 deletion client/i18n/nb.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
6 changes: 5 additions & 1 deletion client/i18n/nn.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
EntityLabel,
InformationProperty,
ProjectLink,
ProjectTag,
UserMessage
} from 'components'
Expand All @@ -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.
Expand Down Expand Up @@ -59,6 +61,39 @@ export const ProjectInformation: StyledComponent = () => {
<EntityLabel key={index} label={label} />
))}
</InformationProperty>
<InformationProperty
hidden={!context.state.selected?.parent}
title={t('projects.parentLabel')}
onRenderValue={() => (
<ProjectLink
project={context.state.selected?.parent}
onClick={() =>
context.dispatch(
SET_SELECTED_PROJECT(context.state.selected?.parent?.tag)
)
}
/>
)}
isDataLoaded={!context.loading}
/>
<InformationProperty
hidden={_.isEmpty(context.state.selected?.children)}
title={t('projects.childrenLabel')}
onRenderValue={() => (
<div style={{ display: 'flex', flexDirection: 'row', gap: 20 }}>
{context.state.selected?.children?.map((child, index) => (
<ProjectLink
key={index}
project={child}
onClick={() =>
context.dispatch(SET_SELECTED_PROJECT(child.tag))
}
/>
))}
</div>
)}
isDataLoaded={!context.loading}
/>
</div>
)
}
Expand Down
6 changes: 6 additions & 0 deletions client/pages/Projects/ProjectForm/BasicInfo/BasicInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
InputControl,
InputControlOptions,
LabelPickerControl,
ProjectPickerControl,
useFormContext
} from 'components/FormControl'
import React from 'react'
Expand Down Expand Up @@ -88,6 +89,11 @@ export const BasicInfo: ProjectFormTabComponent = () => {
)
}
/>
<ProjectPickerControl
{...register('parentKey')}
label={t('projects.parentProject')}
description={t('projects.parentProjectDescription')}
/>
<CreateOutlookCategory />
</FormGroup>
)
Expand Down
12 changes: 8 additions & 4 deletions client/pages/Projects/ProjectForm/useProjectFormSubmit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import $create_or_update_project from './create-or-update-project.gql'
import { CreateOrUpdateProjectVariables, IProjectFormProps } from './types'
import { useProjectFormOptions } from './useProjectFormOptions'
import { useProjectModel } from './useProjectModel'
import _ from 'lodash'

/**
* Creates submit props used by `<FormControl />`.
Expand All @@ -32,10 +33,13 @@ export const useProjectFormSubmit: FormSubmitHook<
*/
async function onClick() {
const variables: CreateOrUpdateProjectVariables = {
project: {
...model.$,
extensions: JSON.stringify(model.$.extensions)
},
project: _.omit(
{
...model.$,
extensions: JSON.stringify(model.$.extensions)
},
['parent', 'children']
),
options: options.$,
update: !!props.edit
}
Expand Down
13 changes: 13 additions & 0 deletions client/pages/Projects/ProjectList/useColumns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,19 @@ export function useColumns(props: IProjectListProps): IListColumn[] {
isMultiline: true
}
),
createColumnDef<Project>(
'parent',
t('projects.parentLabel'),
{
renderAs: 'projectLink',
createRenderProps: (project) => ({
project: project.parent,
onClick: () =>
context.dispatch &&
context.dispatch(SET_SELECTED_PROJECT(project.parent?.tag))
})
}
),
createColumnDef<Project>(
'labels',
t('common.labelFieldLabel'),
Expand Down
15 changes: 15 additions & 0 deletions client/pages/Projects/projects-query.gql
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,21 @@ query ProjectsQuery {
}
inactive
extensions
parentKey
parent {
tag
key
name
description
icon
}
children {
tag
key
name
description
icon
}
}
myProjects {
tag
Expand Down
2 changes: 1 addition & 1 deletion client/pages/Projects/reducer/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ type DATA_UPDTED_PAYLOAD = QueryResult<
>

export const DATA_UPDATED = createAction<DATA_UPDTED_PAYLOAD>('DATA_UPDATED')
export const SET_SELECTED_PROJECT = createAction<Project>(
export const SET_SELECTED_PROJECT = createAction<Project | string>(
'SET_SELECTED_PROJECT'
)
export const OPEN_EDIT_PANEL = createAction<Project>('OPEN_EDIT_PANEL')
Expand Down
9 changes: 8 additions & 1 deletion client/pages/Projects/reducer/useProjectsReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
OPEN_EDIT_PANEL,
SET_SELECTED_PROJECT
} from './actions'
import { Project } from 'types'
import { current } from '@reduxjs/toolkit'

/**
* Use Projects reducer.
Expand Down Expand Up @@ -46,7 +48,12 @@ export function useProjectsReducer() {
state.error = payload.error as any
})
.addCase(SET_SELECTED_PROJECT, (state, { payload }) => {
state.selected = payload
state.selected =
typeof payload === 'string'
? _.find(current(state).projects, ({ tag }) =>
fuzzyStringEqual(tag, payload)
)
: (payload as Project)
})
.addCase(OPEN_EDIT_PANEL, (state, { payload }) => {
state.editProject = payload
Expand Down
Loading

0 comments on commit 86deb49

Please sign in to comment.