Skip to content

Commit 9e1b746

Browse files
authored
Palette filter (#120)
* Added ProjectSettings class and endpoint for toggling filters * Added toggles for the filters in the project settings menu * Filters now apply to palette in the builder, showing only elements from the filters which are set to true * Endpoint for enabling or disabling filters now no longer relies on /toggle, but on /enable or /disable respectively instead. Also added some more styling to the project settings page
1 parent b13c7e0 commit 9e1b746

13 files changed

Lines changed: 292 additions & 9 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import clsx from 'clsx'
2+
3+
interface RoundedToggleProperties {
4+
label: string
5+
enabled?: boolean
6+
onClick?: () => void
7+
className?: string
8+
}
9+
10+
export default function RoundedToggle({
11+
label,
12+
enabled = true,
13+
onClick,
14+
className,
15+
}: Readonly<RoundedToggleProperties>) {
16+
return (
17+
<span
18+
onClick={onClick}
19+
className={clsx(
20+
'cursor-pointer rounded-full border px-3 py-1 text-sm font-medium select-none',
21+
enabled
22+
? 'border-foreground-active hover:bg-foreground-active' // active
23+
: 'border-border/30 text-foreground/30 hover:border-border', // disabled/faded
24+
className,
25+
)}
26+
>
27+
{label}
28+
</span>
29+
)
30+
}

src/main/frontend/app/routes/builder/context/builder-context.tsx

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,20 @@ import SortedElements from '~/routes/builder/context/sorted-elements'
66
import Search from '~/components/search/search'
77
import variables from '../../../../environment/environment'
88
import { useFFDoc } from '@frankframework/ff-doc/react'
9+
import { useProjectStore } from '~/stores/project-store'
910

1011
export default function BuilderContext() {
1112
const { setAttributes, setNodeId } = useNodeContextStore((state) => state)
1213
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({})
1314
const [searchTerm, setSearchTerm] = useState('')
15+
const project = useProjectStore((state) => state.project)
1416
const FRANK_DOC_URL = variables.frankDocJsonUrl
15-
const { elements } = useFFDoc(FRANK_DOC_URL)
17+
const { filters, elements } = useFFDoc(FRANK_DOC_URL)
18+
const enabledFilters = project
19+
? Object.entries(project.filters)
20+
.filter(([_, enabled]) => enabled)
21+
.map(([filterName]) => filterName)
22+
: []
1623

1724
useEffect(() => {
1825
if (!elements) return
@@ -39,6 +46,27 @@ export default function BuilderContext() {
3946
}
4047
}
4148

49+
const shouldShowElement = (elementName: string) => {
50+
// Show all elements if no filters are applied
51+
if (!filters || enabledFilters.length === 0) return true
52+
if (!filters.TYPE || enabledFilters.length === 0) return true
53+
54+
// Check if element exists in any TYPE category
55+
const foundInFilters = Object.values(filters).some((categoryGroup) =>
56+
Object.values(categoryGroup).some((items) => items.includes(elementName)),
57+
)
58+
59+
// If the element is not part of any category then its always visible, this would apply for things like Exits, jobs etc.
60+
if (!foundInFilters) return true
61+
62+
// Otherwise, only show if its category is enabled
63+
return Object.values(filters).some((categoryGroup) =>
64+
Object.entries(categoryGroup).some(
65+
([categoryName, items]) => enabledFilters.includes(categoryName) && items.includes(elementName),
66+
),
67+
)
68+
}
69+
4270
const groupElementsByType = (elements: Record<string, any>) => {
4371
const grouped: Record<string, any[]> = {}
4472
const seen = new Set<string>()
@@ -54,7 +82,11 @@ export default function BuilderContext() {
5482
return grouped
5583
}
5684

57-
const groupedElements = elements ? groupElementsByType(elements) : {}
85+
const visibleElements = Object.fromEntries(
86+
Object.entries(elements || {}).filter(([_, value]) => shouldShowElement(value.name)),
87+
)
88+
89+
const groupedElements = elements ? groupElementsByType(visibleElements) : {}
5890

5991
const filteredGroupedElements = Object.entries(groupedElements).reduce(
6092
(accumulator, [type, items]) => {

src/main/frontend/app/routes/builder/context/sorted-elements.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ interface Properties {
1515
}
1616

1717
export default function SortedElements({ type, items, onDragStart, searchTerm }: Readonly<Properties>) {
18-
const [isExpanded, setIsExpanded] = useState(true)
18+
const [isExpanded, setIsExpanded] = useState(false)
1919

2020
const toggleExpansion = () => {
2121
setIsExpanded(!isExpanded)

src/main/frontend/app/routes/projects/project-tile.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import CodeIcon from '/icons/solar/Code.svg?react'
33
import type { Project } from './projects'
44
import { useNavigate } from 'react-router'
55
import { useProjectStore } from '~/stores/project-store'
6-
import {useTreeStore} from '~/stores/tree-store';
6+
import { useTreeStore } from '~/stores/tree-store'
77

88
interface ProjectTileProperties {
99
project: Project

src/main/frontend/app/routes/projects/projects.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import ProjectTile from '~/routes/projects/project-tile'
44
export interface Project {
55
name: string
66
filenames: string[]
7+
filters: Record<string, boolean> // key = filter name (e.g. "HTTP"), value = true/false
78
}
89

910
export default function Projects() {
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { Link } from 'react-router'
2+
import RoundedToggle from '~/components/inputs/rounded-toggle'
3+
import { useProjectStore } from '~/stores/project-store'
4+
5+
export default function ProjectSettings() {
6+
const project = useProjectStore((state) => state.project)
7+
const setProject = useProjectStore((state) => state.setProject)
8+
9+
const handleToggleFilter = async (filter: string) => {
10+
if (!project) return
11+
12+
const currentlyEnabled = project.filters[filter]
13+
const action = currentlyEnabled ? 'disable' : 'enable'
14+
15+
try {
16+
const response = await fetch(
17+
`http://localhost:8080/projects/${encodeURIComponent(project.name)}/filters/${encodeURIComponent(
18+
filter,
19+
)}/${action}`,
20+
{ method: 'PATCH' },
21+
)
22+
23+
if (!response.ok) throw new Error(`Failed to toggle filter ${filter}`)
24+
25+
// Update the project in the store
26+
const updatedFilters = { ...project.filters, [filter]: !currentlyEnabled }
27+
setProject({ ...project, filters: updatedFilters })
28+
} catch (error) {
29+
console.error(error)
30+
}
31+
}
32+
33+
return (
34+
<div className="space-y-3 p-6">
35+
{project ? (
36+
// Project loaded: show filters
37+
<>
38+
<div className="border-border bg-background rounded-md border p-6">
39+
<h2 className="text-lg font-semibold">{project.name}</h2>
40+
<p className="mb-4">Apply project wide settings</p>
41+
</div>
42+
<div className="border-border bg-background rounded-md border p-6">
43+
<h2 className="text-lg font-semibold">Filters</h2>
44+
<p className="mb-4">Enable or disable filters to only show elements of those types in the studio</p>
45+
<div className="flex flex-wrap gap-2">
46+
{Object.entries(project.filters).map(([filter, enabled]) => (
47+
<RoundedToggle
48+
key={filter}
49+
label={filter}
50+
enabled={enabled}
51+
onClick={() => handleToggleFilter(filter)}
52+
/>
53+
))}
54+
</div>
55+
</div>
56+
</>
57+
) : (
58+
// No project loaded
59+
<div className="border-border bg-background text-muted-foreground flex h-64 items-center justify-center rounded-md border p-6 text-center text-sm">
60+
Load a project in the&nbsp;
61+
<Link to="/" className="font-medium text-blue-600 hover:underline">
62+
Project Overview
63+
</Link>
64+
&nbsp;to adjust the settings
65+
</div>
66+
)}
67+
</div>
68+
)
69+
}

src/main/frontend/app/routes/settings/settings-menu-items.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import WidgetIcon from '/icons/solar/Widget.svg?react'
44
import RulerCrossPenIcon from '/icons/solar/Ruler Cross Pen.svg?react'
55
import CodeIcon from '/icons/solar/Code.svg?react'
66
import GeneralSettings from '~/routes/settings/pages/general-settings'
7+
import ProjectSettings from './pages/project-settings'
78

89
export type SettingsMenuItem = TreeItem<SettingsMenuItemData>
910

@@ -37,6 +38,7 @@ const SettingsMenuItems = {
3738
title: 'Projects',
3839
description: 'Project settings',
3940
icon: WidgetIcon,
41+
content: ProjectSettings,
4042
},
4143
},
4244
studio: {

src/main/java/org/frankframework/flow/project/Project.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,18 @@
22

33
import java.util.ArrayList;
44

5+
import org.frankframework.flow.projectsettings.FilterType;
6+
import org.frankframework.flow.projectsettings.ProjectSettings;
7+
58
public class Project {
69
private String name;
710
private ArrayList<String> filenames;
11+
private ProjectSettings projectSettings;
812

913
public Project(String name) {
1014
this.name = name;
1115
this.filenames = new ArrayList<>();
16+
this.projectSettings = new ProjectSettings();
1217
}
1318

1419
public String getName() {
@@ -25,4 +30,20 @@ public ArrayList<String> getFilenames() {
2530
public void addFilenames(String filename) {
2631
this.filenames.add(filename);
2732
}
33+
34+
public ProjectSettings getProjectSettings(){
35+
return this.projectSettings;
36+
}
37+
38+
public boolean isFilterEnabled(FilterType type) {
39+
return projectSettings.isEnabled(type);
40+
}
41+
42+
public void enableFilter(FilterType type){
43+
projectSettings.setEnabled(type, true);
44+
}
45+
46+
public void disableFilter(FilterType type){
47+
projectSettings.setEnabled(type, false);
48+
}
2849
}

src/main/java/org/frankframework/flow/project/ProjectController.java

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package org.frankframework.flow.project;
22

3+
import org.frankframework.flow.projectsettings.FilterType;
34
import org.springframework.http.ResponseEntity;
45
import org.springframework.web.bind.annotation.GetMapping;
6+
import org.springframework.web.bind.annotation.PatchMapping;
57
import org.springframework.web.bind.annotation.PathVariable;
68
import org.springframework.web.bind.annotation.PostMapping;
79
import org.springframework.web.bind.annotation.RequestMapping;
@@ -24,17 +26,18 @@ public ResponseEntity<List<ProjectDTO>> getAllProjects() {
2426
List<ProjectDTO> projectDTOList = new ArrayList<>();
2527
List<Project> projects = projectService.getProjects();
2628

27-
for (Project project : projects){
29+
for (Project project : projects) {
2830
ProjectDTO projectDTO = new ProjectDTO();
2931
projectDTO.name = project.getName();
3032
projectDTO.filenames = project.getFilenames();
33+
projectDTO.filters = project.getProjectSettings().getFilters();
3134
projectDTOList.add(projectDTO);
3235
}
3336
return ResponseEntity.ok(projectDTOList);
3437
}
3538

3639
@GetMapping("/{projectname}")
37-
public ResponseEntity<ProjectDTO> getProject(@PathVariable String projectname){
40+
public ResponseEntity<ProjectDTO> getProject(@PathVariable String projectname) {
3841
try {
3942
Project project = projectService.getProject(projectname);
4043
if (project == null) {
@@ -43,14 +46,15 @@ public ResponseEntity<ProjectDTO> getProject(@PathVariable String projectname){
4346
ProjectDTO projectDTO = new ProjectDTO();
4447
projectDTO.name = project.getName();
4548
projectDTO.filenames = project.getFilenames();
49+
projectDTO.filters = project.getProjectSettings().getFilters();
4650
return ResponseEntity.ok(projectDTO);
4751
} catch (Exception e) {
4852
return ResponseEntity.badRequest().build();
4953
}
5054
}
5155

5256
@PostMapping("/{projectname}")
53-
public ResponseEntity<ProjectDTO> createProject(@PathVariable String projectname){
57+
public ResponseEntity<ProjectDTO> createProject(@PathVariable String projectname) {
5458
try {
5559
projectService.createProject(projectname);
5660
ProjectDTO projectDTO = new ProjectDTO();
@@ -60,4 +64,68 @@ public ResponseEntity<ProjectDTO> createProject(@PathVariable String projectname
6064
return ResponseEntity.badRequest().build();
6165
}
6266
}
67+
68+
@PatchMapping("/{projectname}/filters/{type}/enable")
69+
public ResponseEntity<ProjectDTO> enableFilter(
70+
@PathVariable String projectname,
71+
@PathVariable String type) {
72+
try {
73+
Project project = projectService.getProject(projectname);
74+
if (project == null) {
75+
return ResponseEntity.notFound().build();
76+
}
77+
78+
// Parse enum safely
79+
FilterType filterType = FilterType.valueOf(type.toUpperCase());
80+
81+
// Enable the filter
82+
project.enableFilter(filterType);
83+
84+
// Return updated DTO
85+
ProjectDTO dto = new ProjectDTO();
86+
dto.name = project.getName();
87+
dto.filenames = project.getFilenames();
88+
dto.filters = project.getProjectSettings().getFilters();
89+
90+
return ResponseEntity.ok(dto);
91+
92+
} catch (IllegalArgumentException e) {
93+
// thrown if invalid type string
94+
return ResponseEntity.badRequest().body(null);
95+
} catch (Exception e) {
96+
return ResponseEntity.internalServerError().build();
97+
}
98+
}
99+
100+
@PatchMapping("/{projectname}/filters/{type}/disable")
101+
public ResponseEntity<ProjectDTO> disableFilter(
102+
@PathVariable String projectname,
103+
@PathVariable String type) {
104+
try {
105+
Project project = projectService.getProject(projectname);
106+
if (project == null) {
107+
return ResponseEntity.notFound().build();
108+
}
109+
110+
// Parse enum safely
111+
FilterType filterType = FilterType.valueOf(type.toUpperCase());
112+
113+
// Disable the filter
114+
project.disableFilter(filterType);
115+
116+
// Return updated DTO
117+
ProjectDTO dto = new ProjectDTO();
118+
dto.name = project.getName();
119+
dto.filenames = project.getFilenames();
120+
dto.filters = project.getProjectSettings().getFilters();
121+
122+
return ResponseEntity.ok(dto);
123+
124+
} catch (IllegalArgumentException e) {
125+
// thrown if invalid type string
126+
return ResponseEntity.badRequest().body(null);
127+
} catch (Exception e) {
128+
return ResponseEntity.internalServerError().build();
129+
}
130+
}
63131
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
package org.frankframework.flow.project;
22

33
import java.util.List;
4+
import java.util.Map;
5+
6+
import org.frankframework.flow.projectsettings.FilterType;
47

58
public class ProjectDTO {
69
public String name;
710
public List<String> filenames;
11+
public Map<FilterType, Boolean> filters;
812
}

0 commit comments

Comments
 (0)