Skip to content

Commit be82bfa

Browse files
EPMRPP-101927 || Ability to change the links order in the footer (#4335)
* EPMRPP-101927 || Ability to change the links order in the footer * EPMRPP-101927 || crf - 1 * EPMRPP-101927 || translations
1 parent 6c51274 commit be82bfa

File tree

15 files changed

+297
-48
lines changed

15 files changed

+297
-48
lines changed

app/localization/translated/be.json

+2
Original file line numberDiff line numberDiff line change
@@ -1255,7 +1255,9 @@
12551255
"LinkIssueModal.urlHint": "Спасылка павінна адпавядаць сапраўднаму адрасу вэб-сайта",
12561256
"LinksAndBrandingTab.addedFooterLinkSuccess": "Спасылка была паспяхова дададзена",
12571257
"LinksAndBrandingTab.deleteFooterLinkSuccess": "Спасылка была паспяхова выдалена",
1258+
"LinksAndBrandingTab.disabledOrderChange": "Дадайце хаця б яшчэ адну спасылку, каб змяніць парадак",
12581259
"LinksAndBrandingTab.formHeader": "Спасылкі ў футары",
1260+
"LinksAndBrandingTab.orderChangedSuccessfully": "Парадак спасылак паспяхова зменены",
12591261
"LinksAndBrandingTab.previewDescription": "Вы можаце наладзіць неабходныя спасылкі ў футары дадатку.",
12601262
"LinksAndBrandingTab.titleForDisabledButton": "Вы дадалі максімальна дапушчальную колькасць спасылак.",
12611263
"LocalizationBlock.belarusian": "Беларуская",

app/localization/translated/es.json

+2
Original file line numberDiff line numberDiff line change
@@ -1255,7 +1255,9 @@
12551255
"LinkIssueModal.urlHint": "El enlace debe ser una dirección web válida",
12561256
"LinksAndBrandingTab.addedFooterLinkSuccess": "The link has been added successfully",
12571257
"LinksAndBrandingTab.deleteFooterLinkSuccess": "The link has been deleted successfully",
1258+
"LinksAndBrandingTab.disabledOrderChange": "Add at least one more link to change the order",
12581259
"LinksAndBrandingTab.formHeader": "Footer Links",
1260+
"LinksAndBrandingTab.orderChangedSuccessfully": "The links order has been changed successfully",
12591261
"LinksAndBrandingTab.previewDescription": "You can configure the necessary links in the footer of the application.",
12601262
"LinksAndBrandingTab.titleForDisabledButton": "You have added the maximum allowed number of links.",
12611263
"LocalizationBlock.belarusian": "Bielorruso",

app/localization/translated/ru.json

+2
Original file line numberDiff line numberDiff line change
@@ -1255,7 +1255,9 @@
12551255
"LinkIssueModal.urlHint": "Ссылка должна соответствовать действительному адресу веб-сайта",
12561256
"LinksAndBrandingTab.addedFooterLinkSuccess": "Ссылка была успешно добавлена",
12571257
"LinksAndBrandingTab.deleteFooterLinkSuccess": "Ссылка была успешно удалена",
1258+
"LinksAndBrandingTab.disabledOrderChange": "Добавьте хотя бы еще одну ссылку, чтобы изменить порядок",
12581259
"LinksAndBrandingTab.formHeader": "Cсылки в футере",
1260+
"LinksAndBrandingTab.orderChangedSuccessfully": "Порядок ссылок успешно изменен",
12591261
"LinksAndBrandingTab.previewDescription": "Вы можете настроить необходимые ссылки в футере приложения.",
12601262
"LinksAndBrandingTab.titleForDisabledButton": "Вы добавили максимально допустимое количество ссылок.",
12611263
"LocalizationBlock.belarusian": "Белорусский",

app/localization/translated/uk.json

+2
Original file line numberDiff line numberDiff line change
@@ -1255,7 +1255,9 @@
12551255
"LinkIssueModal.urlHint": "Посилання має веб-сайту відповідати дійсному адресою",
12561256
"LinksAndBrandingTab.addedFooterLinkSuccess": "Посилання було успішно додано",
12571257
"LinksAndBrandingTab.deleteFooterLinkSuccess": "Посилання було успішно видалено",
1258+
"LinksAndBrandingTab.disabledOrderChange": "Додайте принаймні ще одне посилання, щоб змінити порядок",
12581259
"LinksAndBrandingTab.formHeader": "Посилання у футері",
1260+
"LinksAndBrandingTab.orderChangedSuccessfully": "Порядок посилань успішно змінено",
12591261
"LinksAndBrandingTab.previewDescription": "Ви можете налаштувати необхідні посилання футера застосунку.",
12601262
"LinksAndBrandingTab.titleForDisabledButton": "Ви додали максимально допустиму кількість посилань.",
12611263
"LocalizationBlock.belarusian": "Білоруська",

app/localization/translated/zh.json

+2
Original file line numberDiff line numberDiff line change
@@ -1254,7 +1254,9 @@
12541254
"LinkIssueModal.urlHint": "链接应与有效的网站地址匹配",
12551255
"LinksAndBrandingTab.addedFooterLinkSuccess": "The link has been added successfully",
12561256
"LinksAndBrandingTab.deleteFooterLinkSuccess": "The link has been deleted successfully",
1257+
"LinksAndBrandingTab.disabledOrderChange": "Add at least one more link to change the order",
12571258
"LinksAndBrandingTab.formHeader": "Footer Links",
1259+
"LinksAndBrandingTab.orderChangedSuccessfully": "The links order has been changed successfully",
12581260
"LinksAndBrandingTab.previewDescription": "You can configure the necessary links in the footer of the application.",
12591261
"LinksAndBrandingTab.titleForDisabledButton": "You have added the maximum allowed number of links.",
12601262
"LocalizationBlock.belarusian": "Беларуская",

app/src/components/main/analytics/events/adminServerSettingsPageEvents.js

+37
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ export const submitAnalyticsBtn = (status) => ({
2828
status,
2929
});
3030

31+
const DEFAULT_FOOTER_LINK_NAMES = [
32+
'Contact us',
33+
'Privacy Policy',
34+
'Documentation',
35+
'Slack Channel',
36+
];
37+
3138
export const ADMIN_SERVER_SETTINGS_PAGE_EVENTS = {
3239
toggleSsoUsers: (switcherValue) => ({
3340
...basicClickEventParametersAdminServerSettingsPage,
@@ -39,10 +46,40 @@ export const ADMIN_SERVER_SETTINGS_PAGE_EVENTS = {
3946
element_name: 'session_inactivity_timeout',
4047
condition,
4148
}),
49+
LINKS_AND_BRANDING_TAB: {
50+
...basicClickEventParametersAdminServerSettingsPage,
51+
element_name: 'links_branding',
52+
},
4253
ANALYTICS_TAB: {
4354
...basicClickEventParametersAdminServerSettingsPage,
4455
element_name: 'analytics',
4556
},
57+
ADD_NEW_FOOTER_LINK: {
58+
...basicClickEventParametersAdminServerSettingsPage,
59+
icon_name: 'add_new_link',
60+
place: 'section_footer_links',
61+
},
62+
SAVE_NEW_FOOTER_LINK: {
63+
...basicClickEventParametersAdminServerSettingsPage,
64+
element_name: 'save',
65+
place: 'section_footer_links',
66+
},
67+
DRAG_END_FOOTER_LINK: {
68+
...basicClickEventParametersAdminServerSettingsPage,
69+
place: 'section_footer_links',
70+
icon_name: 'drag_link',
71+
},
72+
onDeleteFooterLink: (linkName) => ({
73+
...basicClickEventParametersAdminServerSettingsPage,
74+
element_name: 'delete',
75+
place: 'section_footer_links',
76+
link_name: DEFAULT_FOOTER_LINK_NAMES.includes(linkName) ? 'default_link' : 'custom_link',
77+
}),
78+
onClickFooterLink: (linkName, isPreview) => ({
79+
...basicClickEventParametersAdminServerSettingsPage,
80+
place: isPreview ? 'preview_footer' : 'footer',
81+
link_name: DEFAULT_FOOTER_LINK_NAMES.includes(linkName) ? 'default_link' : 'custom_link',
82+
}),
4683
// GA3 events
4784
AUTHORIZATION_CONFIGURATION_TAB: {
4885
category: ADMIN_SERVER_SETTINGS_PAGE,

app/src/layouts/common/footer/footer.jsx

+15-16
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ import classNames from 'classnames/bind';
2121
import { FormattedMessage } from 'react-intl';
2222
import { referenceDictionary } from 'common/utils/referenceDictionary';
2323
import { serverFooterLinksSelector, uiBuildVersionSelector } from 'controllers/appInfo';
24-
import { instanceTypeSelector } from 'controllers/appInfo/selectors';
25-
import { EPAM, SAAS } from 'controllers/appInfo/constants';
24+
import { useTracking } from 'react-tracking';
25+
import { ADMIN_SERVER_SETTINGS_PAGE_EVENTS } from 'components/main/analytics/events';
2626
import styles from './footer.scss';
2727

2828
const cx = classNames.bind(styles);
@@ -37,24 +37,15 @@ export const DEFAULT_FOOTER_LINKS = [
3737
},
3838
];
3939

40-
export const PRIVACY_POLICY_LINK = {
41-
name: 'Privacy Policy',
42-
url: referenceDictionary.rpEpamPolicy,
43-
};
4440
const MAX_HEIGHT_ONE_LINE = 35;
4541

46-
export const Footer = ({ className = '' }) => {
42+
export const Footer = ({ className = '', isPreview = false }) => {
4743
const buildVersion = useSelector(uiBuildVersionSelector);
48-
const instanceType = useSelector(instanceTypeSelector);
4944
const customLinks = useSelector(serverFooterLinksSelector);
50-
const links = [
51-
...DEFAULT_FOOTER_LINKS,
52-
...customLinks,
53-
...(instanceType === EPAM || instanceType === SAAS ? [PRIVACY_POLICY_LINK] : []),
54-
];
45+
const links = [...DEFAULT_FOOTER_LINKS, ...customLinks];
5546
const footerRef = useRef(null);
5647
const [isSingleLine, setIsSingleLine] = useState(true);
57-
48+
const { trackEvent } = useTracking();
5849
useEffect(() => {
5950
const checkIsSingleLine = () => {
6051
if (footerRef.current) {
@@ -65,7 +56,9 @@ export const Footer = ({ className = '' }) => {
6556
window.addEventListener('resize', checkIsSingleLine);
6657
return () => window.removeEventListener('resize', checkIsSingleLine);
6758
}, [customLinks]);
68-
59+
const handleLinkClick = (linkName) => {
60+
trackEvent(ADMIN_SERVER_SETTINGS_PAGE_EVENTS.onClickFooterLink(linkName, isPreview));
61+
};
6962
return (
7063
<footer className={cx('footer', { 'one-line': isSingleLine }, className)} ref={footerRef}>
7164
<div className={cx('text-wrapper')}>
@@ -80,7 +73,12 @@ export const Footer = ({ className = '' }) => {
8073
</div>
8174
<div className={cx('footer-links', { 'one-line': isSingleLine })}>
8275
{links.map((link) => (
83-
<a key={link.name} href={link.url} target="_blank">
76+
<a
77+
key={link.name}
78+
href={link.url}
79+
target="_blank"
80+
onClick={() => handleLinkClick(link.name)}
81+
>
8482
{link.name}
8583
</a>
8684
))}
@@ -91,4 +89,5 @@ export const Footer = ({ className = '' }) => {
9189

9290
Footer.propTypes = {
9391
className: PropTypes.string,
92+
isPreview: PropTypes.bool,
9493
};

app/src/layouts/common/footer/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@
1414
* limitations under the License.
1515
*/
1616

17-
export { Footer, DEFAULT_FOOTER_LINKS, PRIVACY_POLICY_LINK } from './footer';
17+
export { Footer, DEFAULT_FOOTER_LINKS } from './footer';

app/src/pages/admin/serverSettingsPage/serverSettingsTabs/linksAndBrandingTab/addLinkForm/addLinkForm.jsx

+5
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import { email } from 'common/utils/validation/validate';
2929
import { NOTIFICATION_TYPES, showNotification } from 'controllers/notification';
3030
import { FormField } from 'components/fields/formField';
3131
import { Input } from 'components/inputs/input';
32+
import { useTracking } from 'react-tracking';
33+
import { ADMIN_SERVER_SETTINGS_PAGE_EVENTS } from 'components/main/analytics/events';
3234
import styles from './addLinkForm.scss';
3335

3436
const cx = classNames.bind(styles);
@@ -49,9 +51,12 @@ const messages = defineMessages({
4951

5052
const AddLink = ({ onClose, handleSubmit, customLinks }) => {
5153
const { formatMessage } = useIntl();
54+
const { trackEvent } = useTracking();
5255
const dispatch = useDispatch();
5356
const handleAddLink = (values) => {
5457
const targetLink = { ...values, url: email(values.url) ? `mailto:${values.url}` : values.url };
58+
trackEvent(ADMIN_SERVER_SETTINGS_PAGE_EVENTS.SAVE_NEW_FOOTER_LINK);
59+
5560
dispatch(
5661
updateServerFooterLinksAction({
5762
footerLinks: [...customLinks, targetLink],
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* Copyright 2025 EPAM Systems
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import React, { useState } from 'react';
18+
import { useDrag, useDrop } from 'react-dnd';
19+
import { Icon } from 'components/main/icon';
20+
import { DragControl } from 'pages/inside/projectSettingsPageContainer/content/elements/ruleList/ruleItem/draggable/dragControl';
21+
import classNames from 'classnames/bind';
22+
import PropTypes from 'prop-types';
23+
import { useTracking } from 'react-tracking';
24+
import { ADMIN_SERVER_SETTINGS_PAGE_EVENTS } from 'components/main/analytics/events';
25+
import styles from '../linksAndBrandingTab.scss';
26+
27+
const cx = classNames.bind(styles);
28+
const FOOTER_LINK_DRAG_SOURCE_TYPE = 'FOOTER_LINK';
29+
export const DraggableLink = ({ item, onDrop, onDelete, disabled, disabledTitle }) => {
30+
const [isDragging, setIsDragging] = useState(false);
31+
const { trackEvent } = useTracking();
32+
const [{ isInDraggingState }, dragRef, dragPreviewRef] = useDrag(
33+
() => ({
34+
type: FOOTER_LINK_DRAG_SOURCE_TYPE,
35+
item: { name: item.name, index: item.index },
36+
collect: (monitor) => ({
37+
isInDraggingState: monitor.isDragging(),
38+
}),
39+
canDrag: () => !disabled,
40+
}),
41+
[item],
42+
);
43+
const [{ draggedItemIndex }, dropRef] = useDrop(
44+
() => ({
45+
accept: FOOTER_LINK_DRAG_SOURCE_TYPE,
46+
collect: (monitor) => {
47+
const draggedItem = monitor.getItem();
48+
const isAbleToDrop = draggedItem?.name !== item.name;
49+
const isOver = isAbleToDrop ? monitor.isOver() : false;
50+
return {
51+
draggedItemIndex: isOver ? draggedItem?.index : null,
52+
};
53+
},
54+
drop: (dragObject) => {
55+
if (dragObject.name !== item.name) {
56+
onDrop(dragObject.index, item.index);
57+
}
58+
},
59+
}),
60+
[item, onDrop],
61+
);
62+
const dropTargetType = draggedItemIndex > item.index ? 'top' : 'bottom';
63+
const handleDragStart = () => {
64+
setIsDragging(true);
65+
};
66+
const handleDragEnd = () => {
67+
trackEvent(ADMIN_SERVER_SETTINGS_PAGE_EVENTS.DRAG_END_FOOTER_LINK);
68+
setIsDragging(false);
69+
};
70+
return (
71+
<div
72+
key={item.name}
73+
className={cx('link-item', {
74+
[`drop-target-${dropTargetType}`]: draggedItemIndex !== null,
75+
})}
76+
ref={(node) => dropRef(dragPreviewRef(node))}
77+
style={{ opacity: isInDraggingState ? 0 : 1 }}
78+
>
79+
<div className={cx('content', { 'is-dragging': isDragging })}>
80+
<div className={cx('link-item-name')}>{item.name}</div>
81+
<div className={cx('link-item-url')}>
82+
{item.url.startsWith('mailto:') ? item.url.slice(7) : item.url}
83+
</div>
84+
</div>
85+
{!isDragging && (
86+
<Icon
87+
type="icon-delete"
88+
className={cx('icon-delete')}
89+
onClick={() => onDelete(item.name)}
90+
/>
91+
)}
92+
<DragControl
93+
dragRef={dragRef}
94+
dragPreviewRef={dragPreviewRef}
95+
handleDragStart={handleDragStart}
96+
handleDragEnd={handleDragEnd}
97+
isDragging={isDragging}
98+
disabled={disabled}
99+
title={disabled ? disabledTitle : ''}
100+
/>
101+
</div>
102+
);
103+
};
104+
105+
DraggableLink.propTypes = {
106+
item: PropTypes.shape({
107+
name: PropTypes.string.isRequired,
108+
url: PropTypes.string.isRequired,
109+
index: PropTypes.number.isRequired,
110+
}).isRequired,
111+
onDrop: PropTypes.func.isRequired,
112+
onDelete: PropTypes.func.isRequired,
113+
disabled: PropTypes.bool,
114+
disabledTitle: PropTypes.string,
115+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* Copyright 2025 EPAM Systems
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
export { DraggableLink } from './draggableLink';

0 commit comments

Comments
 (0)