Skip to content

Commit 9561723

Browse files
authored
Merge branch 'develop' into memory-fix
2 parents f650c8a + b05550a commit 9561723

File tree

12 files changed

+327
-33
lines changed

12 files changed

+327
-33
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
.env.staging
55
.vscode/
66
node_modules/
7+
minio/
78
npm-debug.log
89
dump.rdb
910
static/dist/

client/i18n.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ import {
1818
uk,
1919
sv,
2020
tr,
21-
enIN,
22-
ne
21+
enIN
2322
} from 'date-fns/locale';
2423

2524
import { getPreferredLanguage } from './utils/language-utils';
@@ -101,8 +100,7 @@ export function languageKeyToDateLocale(lang) {
101100
'zh-CN': zhCN,
102101
'zh-TW': zhTW,
103102
tr,
104-
ur: enIN,
105-
ne
103+
ur: enIN
106104
};
107105
return languageMap[lang];
108106
}

client/modules/IDE/components/Editor/index.jsx

Lines changed: 77 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -228,21 +228,73 @@ class Editor extends React.Component {
228228
this._cm.on('keyup', this.handleKeyUp);
229229
}
230230

231-
this._cm.on('keydown', (_cm, e) => {
232-
// Skip hinting if the user is pasting (Ctrl/Cmd+V) or using modifier keys (Ctrl/Alt)
233-
if (
234-
((e.ctrlKey || e.metaKey) && e.key === 'v') ||
235-
e.ctrlKey ||
236-
e.altKey
237-
) {
238-
return;
231+
// Mobile autocomplete support (CM5 IME + contenteditable input)
232+
const triggerHint = (cm) => {
233+
const mode = cm.getOption('mode');
234+
if (mode !== 'css' && mode !== 'javascript') return;
235+
236+
const cursor = cm.getCursor();
237+
const token = cm.getTokenAt(cursor);
238+
239+
// Android keyboards often append a trailing space after each word.
240+
// When that happens, stripping the space so the hinter sees the word.
241+
if (token.string === ' ' && cursor.ch > 0 && cursor.ch === token.end) {
242+
const prevToken = cm.getTokenAt({
243+
line: cursor.line,
244+
ch: cursor.ch - 1
245+
});
246+
if (prevToken.string && /[a-z]/i.test(prevToken.string)) {
247+
cm.replaceRange(
248+
'',
249+
{ line: cursor.line, ch: cursor.ch - 1 },
250+
cursor,
251+
'+trimHint'
252+
);
253+
this.showHint(cm);
254+
return;
255+
}
239256
}
240-
const mode = this._cm.getOption('mode');
241-
if (/^[a-z]$/i.test(e.key) && (mode === 'css' || mode === 'javascript')) {
242-
this.showHint(_cm);
257+
if (token.string && /[a-z]/i.test(token.string)) {
258+
this.showHint(cm);
259+
}
260+
};
261+
262+
// Desktop: fires on each keystroke via CM5's textarea input path.
263+
this._cm.on('change', (_cm, changeObj) => {
264+
if (changeObj.origin !== '+input') return;
265+
if (/[a-z]/i.test(changeObj.text.join(''))) {
266+
triggerHint(_cm);
243267
}
244268
});
245269

270+
// Mobile (word commit): fires when a composed word is accepted.
271+
this._compositionEndHandler = () => {
272+
setTimeout(() => {
273+
if (this._cm) triggerHint(this._cm);
274+
}, 150);
275+
};
276+
this._cm
277+
.getInputField()
278+
.addEventListener('compositionend', this._compositionEndHandler);
279+
280+
// Mobile (per-character): forces CM5 to process composing text
281+
// during typing so autocomplete appears before keyboard dismissal.
282+
this._compositionFlushTimer = null;
283+
this._compositionUpdateHandler = (e) => {
284+
if (!e.data || !/[a-z]/i.test(e.data)) return;
285+
clearTimeout(this._compositionFlushTimer);
286+
this._compositionFlushTimer = setTimeout(() => {
287+
const display = this._cm && this._cm.display;
288+
if (display && display.input && display.input.composing) {
289+
display.input.composing.done = true;
290+
display.input.readFromDOMSoon();
291+
}
292+
}, 200);
293+
};
294+
this._cm
295+
.getInputField()
296+
.addEventListener('compositionupdate', this._compositionUpdateHandler);
297+
246298
this._cm.getWrapperElement().style[
247299
'font-size'
248300
] = `${this.props.fontSize}px`;
@@ -372,6 +424,20 @@ class Editor extends React.Component {
372424
componentWillUnmount() {
373425
if (this._cm) {
374426
this._cm.off('keyup', this.handleKeyUp);
427+
const inputField = this._cm.getInputField();
428+
if (this._compositionEndHandler) {
429+
inputField.removeEventListener(
430+
'compositionend',
431+
this._compositionEndHandler
432+
);
433+
}
434+
if (this._compositionUpdateHandler) {
435+
inputField.removeEventListener(
436+
'compositionupdate',
437+
this._compositionUpdateHandler
438+
);
439+
}
440+
clearTimeout(this._compositionFlushTimer);
375441
}
376442
this.props.provideController(null);
377443
}

client/modules/User/components/CollectionItemRow.jsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useDispatch } from 'react-redux';
66
import { removeFromCollection } from '../../IDE/actions/collections';
77
import { formatDateToString } from '../../../utils/formatDate';
88
import RemoveIcon from '../../../images/close.svg';
9+
import { Tooltip } from '../../../common/Tooltip';
910

1011
const CollectionItemRow = ({ collection, item, isOwner }) => {
1112
const { t } = useTranslation();
@@ -48,13 +49,15 @@ const CollectionItemRow = ({ collection, item, isOwner }) => {
4849
<td>{sketchOwnerUsername}</td>
4950
<td className="collection-row__action-column">
5051
{isOwner && (
51-
<button
52-
className="collection-row__remove-button"
53-
onClick={handleSketchRemove}
54-
aria-label={t('Collection.SketchRemoveARIA')}
55-
>
56-
<RemoveIcon focusable="false" aria-hidden="true" />
57-
</button>
52+
<Tooltip content={t('Collection.SketchRemoveARIA')}>
53+
<button
54+
className="collection-row__remove-button"
55+
onClick={handleSketchRemove}
56+
aria-label={t('Collection.SketchRemoveARIA')}
57+
>
58+
<RemoveIcon focusable="false" aria-hidden="true" />
59+
</button>
60+
</Tooltip>
5861
)}
5962
</td>
6063
</tr>
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import React from 'react';
2+
import thunk from 'redux-thunk';
3+
import configureStore from 'redux-mock-store';
4+
import { reduxRender, screen, fireEvent, act } from '../../../test-utils';
5+
import { initialTestState } from '../../../testData/testReduxStore';
6+
import CollectionItemRow from './CollectionItemRow';
7+
8+
jest.mock('../../../i18n');
9+
10+
const mockStore = configureStore([thunk]);
11+
const store = mockStore(initialTestState);
12+
13+
let subjectProps = {
14+
collection: {
15+
id: 'collection-123',
16+
name: 'Test Collection'
17+
},
18+
item: {
19+
createdAt: '2026-01-15T10:30:00.000Z',
20+
isDeleted: false,
21+
project: {
22+
id: 'project-456',
23+
name: 'My Sketch',
24+
user: { username: 'testuser' },
25+
visibility: 'Public'
26+
}
27+
},
28+
isOwner: true
29+
};
30+
31+
const subject = () => {
32+
reduxRender(
33+
<table>
34+
<tbody>
35+
<CollectionItemRow {...subjectProps} />
36+
</tbody>
37+
</table>,
38+
{ store }
39+
);
40+
};
41+
42+
describe('<CollectionItemRow />', () => {
43+
afterEach(() => {
44+
store.clearActions();
45+
});
46+
47+
it('renders the sketch name as a link', () => {
48+
subject();
49+
const link = screen.getByRole('link', { name: 'My Sketch' });
50+
expect(link).toBeInTheDocument();
51+
expect(link).toHaveAttribute('href', '/testuser/sketches/project-456');
52+
});
53+
54+
it('renders the owner username', () => {
55+
subject();
56+
expect(screen.getByText('testuser')).toBeInTheDocument();
57+
});
58+
59+
it('shows the remove button when user is the owner', () => {
60+
subject();
61+
expect(screen.getByRole('button', { name: /remove/i })).toBeInTheDocument();
62+
});
63+
64+
describe('when user is not the owner', () => {
65+
beforeAll(() => {
66+
subjectProps = { ...subjectProps, isOwner: false };
67+
});
68+
69+
afterAll(() => {
70+
subjectProps = { ...subjectProps, isOwner: true };
71+
});
72+
73+
it('does not show the remove button', () => {
74+
subject();
75+
expect(
76+
screen.queryByRole('button', { name: /remove/i })
77+
).not.toBeInTheDocument();
78+
});
79+
});
80+
81+
it('wraps the remove button with a tooltip', async () => {
82+
subject();
83+
84+
const button = screen.getByRole('button', { name: /remove/i });
85+
await act(async () => {
86+
fireEvent.mouseEnter(button);
87+
});
88+
expect(button).toHaveClass('tooltipped');
89+
});
90+
91+
describe('when the project is deleted', () => {
92+
beforeAll(() => {
93+
subjectProps = {
94+
...subjectProps,
95+
item: {
96+
...subjectProps.item,
97+
isDeleted: true,
98+
project: undefined
99+
}
100+
};
101+
});
102+
103+
it('shows the deleted sketch label', () => {
104+
subject();
105+
expect(screen.getByText('Sketch deleted')).toBeInTheDocument();
106+
});
107+
});
108+
});

client/modules/User/components/NewPasswordForm.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export function NewPasswordForm(props: { resetPasswordToken: string }) {
3434
className="form__input"
3535
aria-label={t('NewPasswordForm.TitleARIA')}
3636
type="password"
37-
id="Password"
37+
id="password"
3838
autoComplete="new-password"
3939
{...field.input}
4040
/>
@@ -49,14 +49,14 @@ export function NewPasswordForm(props: { resetPasswordToken: string }) {
4949
<Field name="confirmPassword">
5050
{(field) => (
5151
<p className="form__field">
52-
<label htmlFor="confirm password" className="form__label">
52+
<label htmlFor="confirmPassword" className="form__label">
5353
{t('NewPasswordForm.ConfirmPassword')}
5454
</label>
5555
<input
5656
className="form__input"
5757
type="password"
5858
aria-label={t('NewPasswordForm.ConfirmPasswordARIA')}
59-
id="confirm password"
59+
id="confirmPassword"
6060
autoComplete="new-password"
6161
{...field.input}
6262
/>

client/styles/abstracts/_variables.scss

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ $themes: (
5757
error-color: $p5js-pink,
5858
file-hover-color: $light,
5959
file-selected-color: $medium-light,
60-
form-input-placeholder-text-color: $middle-light,
60+
form-input-placeholder-text-color: $middle-gray,
6161
form-input-text-color: $dark,
6262
form-navigation-options-color: $middle-dark,
6363
form-secondary-title-color: $medium-dark,
@@ -91,7 +91,7 @@ $themes: (
9191
ide-border-color: $medium-light,
9292
inactive-text-color: $middle-dark,
9393
input-background-color: $lightest,
94-
input-border-color: $middle-light,
94+
input-border-color: $middle-dark,
9595
input-secondary-background-color: $lightest,
9696
input-selection-background-color: $medium-light,
9797
input-selection-text-color: $dark,
@@ -155,6 +155,7 @@ $themes: (
155155
error-color: $p5js-pink,
156156
file-hover-color: $dark,
157157
file-selected-color: $medium-dark,
158+
form-input-placeholder-text-color: $middle-gray,
158159
form-navigation-options-color: $middle-light,
159160
form-secondary-title-color: $medium-light,
160161
form-title-color: $lightest,
@@ -187,7 +188,7 @@ $themes: (
187188
ide-border-color: $middle-dark,
188189
inactive-text-color: $middle-light,
189190
input-background-color: $dark,
190-
input-border-color: $middle-dark,
191+
input-border-color: $light,
191192
input-secondary-background-color: $medium-dark,
192193
input-selection-background-color: $lightest,
193194
input-selection-text-color: $darkest,

client/styles/components/_collection.scss

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,22 @@
148148
.collection-row__action-column {
149149
width: #{math.div(60, $base-font-size)}rem;
150150
position: relative;
151+
152+
.tooltip-wrapper {
153+
display: inline-flex;
154+
width: auto;
155+
156+
.tooltipped::after {
157+
right: 0;
158+
left: auto;
159+
}
160+
161+
.tooltipped-n::before,
162+
.tooltipped::before {
163+
right: #{math.div(10, $base-font-size)}rem;
164+
left: auto;
165+
}
166+
}
151167
}
152168

153169
.collection-row__remove-button {

common/p5Versions.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export const currentP5Version = '1.11.11'; // Don't update to 2.x until 2026
1+
export const currentP5Version = '1.11.12'; // Don't update to 2.x until 2026
22

33
// Generated from https://www.npmjs.com/package/p5?activeTab=versions
44
// Run this in the console:
@@ -17,7 +17,8 @@ export const p5Versions = [
1717
'2.0.2',
1818
'2.0.1',
1919
'2.0.0',
20-
{ version: '1.11.11', label: '(Default)' },
20+
{ version: '1.11.12', label: '(Default)' },
21+
'1.11.11',
2122
'1.11.10',
2223
'1.11.9',
2324
'1.11.8',

0 commit comments

Comments
 (0)