Skip to content

Commit 0fa9d75

Browse files
authored
Merge branch 'develop' into fix/User-Model
2 parents cff199d + 71e272e commit 0fa9d75

78 files changed

Lines changed: 20878 additions & 26473 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1-
Fixes #issue-number
1+
### Issue:
2+
Fixes #
3+
<!-- Please add the issue this relates to, and a provide a brief description of the current state of the codebase and what this PR attempts to fix. -->
24

3-
Changes:
5+
### Demo:
6+
<!-- Please add a screenshot for UI related changes -->
47

5-
I have verified that this pull request:
8+
### Changes:
9+
<!-- Summarise your changes -->
10+
11+
### I have verified that this pull request:
612

713
* [ ] has no linting errors (`npm run lint`)
814
* [ ] has no test errors (`npm run test`)

client/common/Tooltip.test.tsx

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import React from 'react';
2+
import userEvent from '@testing-library/user-event';
3+
import { render, screen } from '../test-utils';
4+
import { Tooltip } from './Tooltip';
5+
6+
describe('Tooltip', () => {
7+
it('renders the child element', () => {
8+
render(
9+
<Tooltip content="This is a tooltip">
10+
<button>Hover me</button>
11+
</Tooltip>
12+
);
13+
expect(screen.getByRole('button')).toBeInTheDocument();
14+
expect(screen.getByText('Hover me')).toBeInTheDocument();
15+
});
16+
17+
it('does not show the tooltip when the user is not hovering over the element', () => {
18+
render(
19+
<Tooltip content="Tooltip text">
20+
<button>Button</button>
21+
</Tooltip>
22+
);
23+
24+
const button = screen.getByRole('button');
25+
expect(button).toBeInTheDocument();
26+
expect(button).not.toHaveClass('tooltipped-visible');
27+
});
28+
29+
it('shows the tooltip if the user hovers over the element', async () => {
30+
const user = userEvent.setup();
31+
render(
32+
<Tooltip content="Tooltip text">
33+
<button>Button</button>
34+
</Tooltip>
35+
);
36+
37+
const button = screen.getByRole('button');
38+
await user.hover(button);
39+
40+
expect(button).toHaveClass('tooltipped');
41+
expect(button).toHaveAttribute('aria-label', 'Tooltip text');
42+
});
43+
44+
it('adds the aria-label with tooltip content to the child element', () => {
45+
render(
46+
<Tooltip content="Save your changes">
47+
<button>Save</button>
48+
</Tooltip>
49+
);
50+
51+
const button = screen.getByRole('button');
52+
expect(button).toHaveAttribute('aria-label', 'Save your changes');
53+
});
54+
55+
it('applies tooltipped-no-delay class when noDelay is true', () => {
56+
render(
57+
<Tooltip content="No delay tooltip" noDelay>
58+
<button>Button</button>
59+
</Tooltip>
60+
);
61+
62+
const button = screen.getByRole('button');
63+
expect(button).toHaveClass('tooltipped-no-delay');
64+
});
65+
66+
it('does not apply tooltipped-no-delay class when noDelay is false', () => {
67+
render(
68+
<Tooltip content="Normal tooltip" noDelay={false}>
69+
<button>Button</button>
70+
</Tooltip>
71+
);
72+
73+
const button = screen.getByRole('button');
74+
expect(button).not.toHaveClass('tooltipped-no-delay');
75+
});
76+
77+
it('preserves existing className on the child element', () => {
78+
render(
79+
<Tooltip content="Tooltip">
80+
<button className="custom-class">Button</button>
81+
</Tooltip>
82+
);
83+
84+
const button = screen.getByRole('button');
85+
expect(button).toHaveClass('custom-class');
86+
expect(button).toHaveClass('tooltipped');
87+
});
88+
89+
it('wraps the child in a tooltip-wrapper span', () => {
90+
const { container } = render(
91+
<Tooltip content="Tooltip">
92+
<button>Button</button>
93+
</Tooltip>
94+
);
95+
96+
const wrapper = container.querySelector('.tooltip-wrapper');
97+
expect(wrapper).toBeInTheDocument();
98+
expect(wrapper?.tagName.toLowerCase()).toBe('span');
99+
});
100+
});

client/common/Tooltip.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import React, { ReactElement, useMemo } from 'react';
2+
3+
export type TooltipProps = {
4+
content: string;
5+
noDelay?: boolean;
6+
children: ReactElement;
7+
};
8+
9+
export function Tooltip({ content, noDelay = false, children }: TooltipProps) {
10+
const tooltipClasses = useMemo(() => {
11+
const existingClassName = children.props?.className || '';
12+
return [
13+
existingClassName,
14+
'tooltipped',
15+
'tooltipped-n',
16+
noDelay && 'tooltipped-no-delay'
17+
]
18+
.filter(Boolean)
19+
.join(' ');
20+
}, [children.props?.className, noDelay]);
21+
22+
const childProps = useMemo(
23+
() => ({
24+
'aria-label': content,
25+
className: tooltipClasses
26+
}),
27+
[content, tooltipClasses]
28+
);
29+
30+
return (
31+
<span className="tooltip-wrapper">
32+
{React.cloneElement(children, childProps)}
33+
</span>
34+
);
35+
}

client/components/Menubar/MenubarItem.tsx

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { useEffect, useContext, useRef } from 'react';
22
import { MenubarContext, SubmenuContext, ParentMenuContext } from './contexts';
33
import { ButtonOrLink, ButtonOrLinkProps } from '../../common/ButtonOrLink';
4+
import { Tooltip, TooltipProps } from '../../common/Tooltip';
45

56
export enum MenubarItemRole {
67
MENU_ITEM = 'menuitem',
@@ -13,6 +14,7 @@ export interface MenubarItemProps extends Omit<ButtonOrLinkProps, 'role'> {
1314
*/
1415
role?: MenubarItemRole;
1516
selected?: boolean;
17+
tooltipContent?: TooltipProps['content'];
1618
}
1719

1820
/**
@@ -54,6 +56,7 @@ export function MenubarItem({
5456
role: customRole = MenubarItemRole.MENU_ITEM,
5557
isDisabled = false,
5658
selected = false,
59+
tooltipContent,
5760
...rest
5861
}: MenubarItemProps) {
5962
const { createMenuItemHandlers, hasFocus } = useContext(MenubarContext);
@@ -94,15 +97,29 @@ export function MenubarItem({
9497
ref={menuItemRef}
9598
onMouseEnter={handleMouseEnter}
9699
>
97-
<ButtonOrLink
98-
{...rest}
99-
{...handlers}
100-
{...ariaSelected}
101-
role={role}
102-
tabIndex={-1}
103-
id={id}
104-
isDisabled={isDisabled}
105-
/>
100+
{tooltipContent ? (
101+
<Tooltip content={tooltipContent}>
102+
<ButtonOrLink
103+
{...rest}
104+
{...handlers}
105+
{...ariaSelected}
106+
role={role}
107+
tabIndex={-1}
108+
id={id}
109+
isDisabled={isDisabled}
110+
/>
111+
</Tooltip>
112+
) : (
113+
<ButtonOrLink
114+
{...rest}
115+
{...handlers}
116+
{...ariaSelected}
117+
role={role}
118+
tabIndex={-1}
119+
id={id}
120+
isDisabled={isDisabled}
121+
/>
122+
)}
106123
</li>
107124
);
108125
}

client/constants.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ export const RESET_PROJECT = 'RESET_PROJECT';
3333

3434
export const SET_PROJECT = 'SET_PROJECT';
3535
export const SET_PROJECTS = 'SET_PROJECTS';
36+
export const SET_PROJECTS_FOR_COLLECTION_LIST =
37+
'SET_PROJECTS_FOR_COLLECTION_LIST';
3638

3739
export const SET_COLLECTIONS = 'SET_COLLECTIONS';
3840
export const CREATE_COLLECTION = 'CREATED_COLLECTION';

client/modules/About/pages/About.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ export const About = () => {
167167
<a
168168
href="https://github.com/processing/p5.js-web-editor/releases"
169169
target="_blank"
170-
rel="noreferrer"
170+
rel="noopener noreferrer"
171171
>
172172
{t('About.WebEditor')}: <span>v{packageData?.version}</span>
173173
</a>
@@ -176,7 +176,7 @@ export const About = () => {
176176
<a
177177
href="https://github.com/processing/p5.js/releases"
178178
target="_blank"
179-
rel="noreferrer"
179+
rel="noopener noreferrer"
180180
>
181181
p5.js: <span>v{p5version}</span>
182182
</a>

client/modules/IDE/actions/assets.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ export function getAssets() {
3030
export function deleteAssetRequest(assetKey) {
3131
return async (dispatch) => {
3232
try {
33-
await apiClient.delete(`/S3/${assetKey}`);
33+
const path = assetKey.split('/').pop();
34+
await apiClient.delete(
35+
`/S3/delete?objectKey=${encodeURIComponent(path)}`
36+
);
3437
dispatch(deleteAsset(assetKey));
3538
} catch (error) {
3639
dispatch({

client/modules/IDE/actions/project.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,7 @@ export function changeProjectName(id, newName) {
386386
}
387387

388388
export function deleteProject(id) {
389-
return (dispatch, getState) => {
389+
return (dispatch, getState) =>
390390
apiClient
391391
.delete(`/projects/${id}`)
392392
.then(() => {
@@ -411,7 +411,6 @@ export function deleteProject(id) {
411411
});
412412
}
413413
});
414-
};
415414
}
416415
export function changeVisibility(projectId, projectName, visibility, t) {
417416
return (dispatch, getState) => {

client/modules/IDE/actions/projects.js

Lines changed: 57 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,60 @@ import { apiClient } from '../../../utils/apiClient';
22
import * as ActionTypes from '../../../constants';
33
import { startLoader, stopLoader } from '../reducers/loading';
44

5-
// eslint-disable-next-line
6-
export function getProjects(username) {
7-
return (dispatch) => {
8-
dispatch(startLoader());
9-
let url;
10-
if (username) {
11-
url = `/${username}/projects`;
12-
} else {
13-
url = '/projects';
14-
}
15-
return apiClient
16-
.get(url)
17-
.then((response) => {
18-
dispatch({
19-
type: ActionTypes.SET_PROJECTS,
20-
projects: response.data
21-
});
22-
dispatch(stopLoader());
23-
})
24-
.catch((error) => {
25-
dispatch({
26-
type: ActionTypes.ERROR,
27-
error: error?.response?.data
28-
});
29-
dispatch(stopLoader());
30-
});
31-
};
32-
}
5+
const buildProjectsUrl = (username, options = {}) => {
6+
const {
7+
page = 1,
8+
limit = 10,
9+
sortField = 'updatedAt',
10+
sortDir = 'desc',
11+
q = ''
12+
} = options;
13+
14+
const base = username
15+
? `/${encodeURIComponent(username)}/projects`
16+
: '/projects';
17+
18+
const params = new URLSearchParams({
19+
page: String(page),
20+
limit: String(limit),
21+
sortField,
22+
sortDir
23+
});
24+
25+
const trimmed = q.trim();
26+
27+
if (trimmed) {
28+
params.set('q', trimmed);
29+
}
30+
31+
return `${base}?${params.toString()}`;
32+
};
33+
34+
const fetchProjects = (username, options, successType) => (dispatch) => {
35+
dispatch(startLoader());
36+
37+
const url = buildProjectsUrl(username, options);
38+
39+
return apiClient
40+
.get(url)
41+
.then((response) => {
42+
dispatch({ type: successType, projects: response.data });
43+
dispatch(stopLoader());
44+
return response.data;
45+
})
46+
.catch((error) => {
47+
dispatch({ type: ActionTypes.ERROR, error: error?.response?.data });
48+
dispatch(stopLoader());
49+
throw error;
50+
});
51+
};
52+
53+
export const getProjects = (username, options) =>
54+
fetchProjects(username, options, ActionTypes.SET_PROJECTS);
55+
56+
export const getProjectsForCollectionList = (username, options) =>
57+
fetchProjects(
58+
username,
59+
options,
60+
ActionTypes.SET_PROJECTS_FOR_COLLECTION_LIST
61+
);

0 commit comments

Comments
 (0)