Skip to content

Commit 010c2b2

Browse files
committed
feat(FR-1502): implement editable folder names with visual identicons in file explorer (#4319)
Resolves #4318 ([FR-1502](https://lablup.atlassian.net/browse/FR-1502)) # Enhance VFolder Name Editing and Display in Folder Explorer This PR improves the folder name editing experience and updates the folder explorer header with a more consistent design: 1. Adds title attribute to display full folder name on hover 2. Prevents unnecessary rename mutations when the name hasn't changed 3. Enhances `EditableVFolderName` component with additional props: - New `inputProps` to customize the input field - Support for passing editable props through to the component 4. Replaces `VFolderNameTitle` with `EditableVFolderName` in the folder explorer header 5. Improves the styling of folder identicons for better visual consistency 6. Temporarily comments out the folder name validator to address issues with duplicate name detection **Checklist:** - [ ] Documentation - [ ] Minium required manager version - [ ] Specific setting for review (eg., KB link, endpoint or how to setup) - [ ] Minimum requirements to check during review - [ ] Test case(s) to demonstrate the difference of before/after [FR-1502]: https://lablup.atlassian.net/browse/FR-1502?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
1 parent 612b477 commit 010c2b2

File tree

8 files changed

+106
-32
lines changed

8 files changed

+106
-32
lines changed

packages/backend.ai-ui/src/hooks/useErrorMessageResolver.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const useErrorMessageResolver = () => {
4141
let errorMsg = defaultMessage || t('error.UnknownError');
4242
if (!error) return errorMsg;
4343

44+
// Prioritize `msg` or `message`(deprecated) field if available.
4445
if (error?.msg || error?.message) {
4546
const integratedErrorMsg = error?.msg || error?.message || '';
4647
errorMsg = !_.includes(integratedErrorMsg, 'Traceback')

react/src/components/EditableVFolderName.tsx

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,15 @@ import { useTanMutation } from '../hooks/reactQueryAlias';
66
import { useCurrentProjectValue } from '../hooks/useCurrentProject';
77
import { isDeletedCategory } from '../pages/VFolderNodeListPage';
88
import { useFolderExplorerOpener } from './FolderExplorerOpener';
9-
import { theme, Form, Input, App, GetProps, Typography } from 'antd';
9+
import {
10+
theme,
11+
Form,
12+
Input,
13+
App,
14+
GetProps,
15+
Typography,
16+
InputProps,
17+
} from 'antd';
1018
import { BAILink, toLocalId, useErrorMessageResolver } from 'backend.ai-ui';
1119
import _ from 'lodash';
1220
import { CornerDownLeftIcon } from 'lucide-react';
@@ -23,6 +31,7 @@ type EditableVFolderNameProps = {
2331
vfolderFrgmt: EditableVFolderNameFragment$key;
2432
enableLink?: boolean;
2533
existingNames?: Array<string>;
34+
inputProps?: InputProps;
2635
onEditEnd?: () => void;
2736
onEditStart?: () => void;
2837
} & (
@@ -45,6 +54,7 @@ const EditableVFolderName: React.FC<EditableVFolderNameProps> = ({
4554
existingNames,
4655
onEditEnd,
4756
onEditStart,
57+
inputProps,
4858
...otherProps
4959
}) => {
5060
const vfolder = useFragment(
@@ -105,6 +115,7 @@ const EditableVFolderName: React.FC<EditableVFolderNameProps> = ({
105115
onEditEnd?.();
106116
},
107117
triggerType: ['icon'],
118+
...(!_.isBoolean(editableOfProps) ? editableOfProps : {}),
108119
}
109120
: false
110121
}
@@ -114,6 +125,7 @@ const EditableVFolderName: React.FC<EditableVFolderNameProps> = ({
114125
? token.colorTextTertiary
115126
: style?.color,
116127
}}
128+
title={vfolder.name || undefined}
117129
{...otherProps}
118130
>
119131
{enableLink && !isEditing && (
@@ -132,6 +144,9 @@ const EditableVFolderName: React.FC<EditableVFolderNameProps> = ({
132144
<Form
133145
onFinish={(values) => {
134146
setIsEditing(false);
147+
if (values.vfolderName === vfolder.name) {
148+
return;
149+
}
135150
setOptimisticName(values.vfolderName);
136151
renameMutation.mutate(
137152
{
@@ -159,7 +174,16 @@ const EditableVFolderName: React.FC<EditableVFolderNameProps> = ({
159174
},
160175
onError: (error) => {
161176
onEditEnd?.();
162-
message.error(getErrorMessage(error));
177+
const errorMessage = getErrorMessage(error);
178+
if (
179+
errorMessage.includes(
180+
'One of your accessible vfolders already has the name you requested.',
181+
)
182+
) {
183+
message.error(t('data.FolderAlreadyExists'));
184+
} else {
185+
message.error(errorMessage);
186+
}
163187
setOptimisticName(vfolder.name);
164188
},
165189
},
@@ -188,14 +212,15 @@ const EditableVFolderName: React.FC<EditableVFolderNameProps> = ({
188212
pattern: /^[a-zA-Z0-9-_.]+$/,
189213
message: t('data.AllowsLettersNumbersAnd-_Dot'),
190214
},
191-
{
192-
validator: (__, value) => {
193-
if (_.includes(existingNames, value)) {
194-
return Promise.reject(t('data.FolderAlreadyExists'));
195-
}
196-
return Promise.resolve();
197-
},
198-
},
215+
// TODO: (Priority low) implement async validator to check existing folder names
216+
// {
217+
// validator: (__, value) => {
218+
// if (_.includes(existingNames, value)) {
219+
// return Promise.reject(t('data.FolderAlreadyExists'));
220+
// }
221+
// return Promise.resolve();
222+
// },
223+
// },
199224
]}
200225
style={{
201226
margin: 0,
@@ -219,6 +244,7 @@ const EditableVFolderName: React.FC<EditableVFolderNameProps> = ({
219244
onEditEnd?.();
220245
}
221246
}}
247+
{...inputProps}
222248
/>
223249
</Form.Item>
224250
</Form>

react/src/components/FolderExplorerHeader.tsx

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { FolderExplorerHeaderFragment$key } from '../__generated__/FolderExplorerHeaderFragment.graphql';
2-
import VFolderNameTitle from './VFolderNameTitle';
3-
import { Button, Tooltip, Image, Skeleton, Grid, theme } from 'antd';
2+
import EditableVFolderName from './EditableVFolderName';
3+
import VFolderNodeIdenticon from './VFolderNodeIdenticon';
4+
import { Button, Tooltip, Image, Grid, theme, Typography } from 'antd';
45
import { BAIFlex } from 'backend.ai-ui';
5-
import React, { LegacyRef, Suspense } from 'react';
6+
import React, { LegacyRef } from 'react';
67
import { useTranslation } from 'react-i18next';
78
import { graphql, useFragment } from 'react-relay';
89

@@ -29,6 +30,8 @@ const FolderExplorerHeader: React.FC<FolderExplorerHeaderProps> = ({
2930
permission
3031
unmanaged_path @since(version: "25.04.0")
3132
...VFolderNameTitleNodeFragment
33+
...VFolderNodeIdenticonFragment
34+
...EditableVFolderNameFragment
3235
}
3336
`,
3437
vfolderNodeFrgmt,
@@ -43,20 +46,52 @@ const FolderExplorerHeader: React.FC<FolderExplorerHeaderProps> = ({
4346
>
4447
<BAIFlex
4548
data-testid="folder-explorer-title"
46-
gap={token.marginMD}
49+
gap={'xs'}
4750
style={{ flex: 1, ...titleStyle }}
4851
>
49-
<Suspense fallback={<Skeleton.Input active />}>
50-
<VFolderNameTitle vfolderNodeFrgmt={vfolderNode} />
51-
</Suspense>
52+
{vfolderNode ? (
53+
<VFolderNodeIdenticon
54+
vfolderNodeIdenticonFrgmt={vfolderNode}
55+
style={{
56+
fontSize: token.fontSizeHeading4,
57+
}}
58+
/>
59+
) : (
60+
<BAIFlex
61+
style={{
62+
borderColor: token.colorBorderSecondary,
63+
borderWidth: 1,
64+
borderStyle: 'solid',
65+
width: token.fontSizeHeading3,
66+
height: token.fontSizeHeading3,
67+
borderRadius: token.borderRadius,
68+
}}
69+
/>
70+
)}
71+
{vfolderNode && (
72+
<EditableVFolderName
73+
vfolderFrgmt={vfolderNode}
74+
enableLink={false}
75+
component={Typography.Title}
76+
level={3}
77+
style={{
78+
margin: 0,
79+
width: '100%',
80+
}}
81+
ellipsis
82+
editable={{
83+
triggerType: ['icon', 'text'],
84+
}}
85+
inputProps={{
86+
size: 'large',
87+
}}
88+
/>
89+
)}
5290
</BAIFlex>
5391
<BAIFlex
5492
data-testid="folder-explorer-actions"
5593
justify="end"
5694
gap={token.marginSM}
57-
style={{
58-
flex: lg ? 2 : 1,
59-
}}
6095
>
6196
{!vfolderNode?.unmanaged_path ? (
6297
<>

react/src/components/VFolderNodeIdenticon.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { VFolderNodeIdenticonFragment$key } from '../__generated__/VFolderNodeIdenticonFragment.graphql';
22
import { shapes } from '@dicebear/collection';
33
import { createAvatar } from '@dicebear/core';
4+
import { theme } from 'antd';
45
import React from 'react';
56
import { graphql, useFragment } from 'react-relay';
67

@@ -11,8 +12,9 @@ interface VFolderNodeIdenticonProps {
1112

1213
const VFolderNodeIdenticon: React.FC<VFolderNodeIdenticonProps> = ({
1314
vfolderNodeIdenticonFrgmt,
14-
...style
15+
style,
1516
}) => {
17+
const { token } = theme.useToken();
1618
const vfolder = useFragment(
1719
graphql`
1820
fragment VFolderNodeIdenticonFragment on VirtualFolderNode {
@@ -24,7 +26,15 @@ const VFolderNodeIdenticon: React.FC<VFolderNodeIdenticonProps> = ({
2426

2527
return (
2628
<img
27-
{...style}
29+
style={{
30+
borderRadius: '0.25em',
31+
width: '1em',
32+
height: '1em',
33+
borderWidth: 0.5,
34+
borderStyle: 'solid',
35+
borderColor: token.colorBorder,
36+
...style,
37+
}}
2838
src={createAvatar(shapes, {
2939
seed: vfolder?.id,
3040
shape3: [],

react/src/components/VFolderNodes.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,9 +151,7 @@ const VFolderNodes: React.FC<VFolderNodesProps> = ({
151151
<VFolderNodeIdenticon
152152
vfolderNodeIdenticonFrgmt={vfolder}
153153
style={{
154-
width: token.size,
155-
height: token.size,
156-
borderRadius: token.borderRadiusSM,
154+
fontSize: token.fontSizeHeading5,
157155
}}
158156
/>
159157
{vfolder?.id === hoveredColumn ? (
@@ -171,7 +169,6 @@ const VFolderNodes: React.FC<VFolderNodesProps> = ({
171169
onEditStart={() => {
172170
setEditingColumn(vfolder?.id);
173171
}}
174-
existingNames={_.compact(_.map(filteredVFolders, 'name'))}
175172
/>
176173
) : (
177174
<BAILink

resources/i18n/el.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@
238238
"EnterDifferentValue": "Εισαγάγετε μια τιμή διαφορετική από το υπάρχον όνομα αρχείου ή φακέλου. Ελέγξτε αν ο χαρακτήρας διαστήματος στο πεδίο εισαγωγής ή όχι.",
239239
"ExistingFolderName": "Το υπάρχον όνομα φακέλου",
240240
"FileAndFolderNameRequired": "Απαιτείται όνομα αρχείου / φακέλου",
241+
"FolderAlreadyExists": "Υπάρχει ήδη ένας φάκελος με αυτό το όνομα",
241242
"FolderInfo": "Πληροφορίες για τον Φόρντερ",
242243
"FolderNameRequired": "Απαιτείται όνομα φακέλου",
243244
"FolderNameTooLong": "Εισαγάγετε όνομα φακέλου με λιγότερους από 64 χαρακτήρες",
@@ -259,6 +260,7 @@
259260
"NewFolder": "Νέος φάκελος",
260261
"NewFolderName": "Νέο όνομα φακέλου",
261262
"NoStorageDescriptionFound": "Χωρίς περιγραφή.",
263+
"NotInProject": "Αυτός ο φάκελος ανήκει σε ένα διαφορετικό έργο.",
262264
"NumberOfFolders": "Αριθμός φακέλων",
263265
"Permission": "Αδεια",
264266
"Pipeline": "Γραμμή σωλήνων",
@@ -478,8 +480,7 @@
478480
"Status": "Κατάσταση",
479481
"StatusOfSelectedHost": "Κατάσταση του επιλεγμένου κεντρικού υπολογιστή",
480482
"Used": "χρησιμοποιημένο"
481-
},
482-
"NotInProject": "Αυτός ο φάκελος ανήκει σε ένα διαφορετικό έργο."
483+
}
483484
},
484485
"desktopNotification": {
485486
"NotSupported": "Αυτό το πρόγραμμα περιήγησης δεν υποστηρίζει ειδοποιήσεις.",

resources/i18n/ko.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -240,9 +240,9 @@
240240
"EnterDifferentValue": "기존 파일 또는 폴더 이름과 다른 값을 입력해 주세요. 변경하려는 폴더 이름에 공백문자가 있는지 확인해 주세요.",
241241
"ExistingFolderName": "기존 폴더 이름",
242242
"FileAndFolderNameRequired": "파일 및 폴더 이름이 필요합니다.",
243-
"FolderAlreadyExists": "다음 폴더는 이미 존재합니다.",
243+
"FolderAlreadyExists": "같은 이름의 폴더가 이미 존재합니다.",
244244
"FolderInfo": "폴더 정보",
245-
"FolderNameRequired": "폴더 이름은 필수 입력입니다.",
245+
"FolderNameRequired": "폴더 이름을 입력해주세요.",
246246
"FolderNameTooLong": "폴더 이름은 64자 이하로 입력하세요",
247247
"FolderToCopy": "복사할 폴더 이름",
248248
"Foldername": "폴더 이름",
@@ -263,6 +263,7 @@
263263
"NewFolder": "새 폴더",
264264
"NewFolderName": "새 폴더 이름",
265265
"NoStorageDescriptionFound": "별도 설명이 없습니다.",
266+
"NotInProject": "이 폴더는 다른 프로젝트에 속해 있습니다.",
266267
"NumberOfFolders": "폴더 개수",
267268
"Permission": "권한",
268269
"Pipeline": "파이프라인",
@@ -483,8 +484,7 @@
483484
"Status": "상태",
484485
"StatusOfSelectedHost": "선택한 호스트 상태",
485486
"Used": "사용중"
486-
},
487-
"NotInProject": "이 폴더는 다른 프로젝트에 속해 있습니다."
487+
}
488488
},
489489
"desktopNotification": {
490490
"NotSupported": "현재 브라우저에서는 알림 기능을 지원하지 않습니다. ",

src/lib/backend.ai-client-esm.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,10 @@ class Client {
499499
statusCode: resp.status,
500500
statusText: resp.statusText,
501501
title: errorTitle,
502+
// `msg` field was introduced in v24.09.0
503+
msg: resp.msg,
504+
// `message` is deprecated, but it is kept for backward compatibility.
505+
// use `msg` field instead.
502506
message: errorMsg,
503507
description: errorDesc,
504508
error_code: errorCode,

0 commit comments

Comments
 (0)