Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
50235aa
refactor: streamline notification settings and improve styling compon…
iDome89 Nov 14, 2025
99eb6d2
refactor: adjust flex properties in styled components for better resp…
d-beezee Nov 17, 2025
5399068
- Introduced chevron-up-stroke.svg icon for UI enhancements.
iDome89 Nov 17, 2025
558d80f
rework: implement generic WatcherList component
d-beezee Nov 17, 2025
db0488b
feat: Add WatcherList component and related hooks for campaign watche…
d-beezee Nov 17, 2025
9b70dc7
feat: Use all available users in campaign member add
d-beezee Nov 17, 2025
57ee2d5
i18n: Translate campaign messages
d-beezee Nov 18, 2025
024b81f
feat: Move WatcherList component in mother dashboard
d-beezee Nov 18, 2025
7d1460a
feat: Add WatcherList component to Tools for bugs list
d-beezee Nov 18, 2025
8c6f1ac
feat: Hide watcherlist in archived campaigns
d-beezee Nov 18, 2025
66c4f68
Merge branch 'develop' into UN-2060
d-beezee Nov 21, 2025
04e44a6
Merge pull request #1469 from AppQuality/UN-2061
d-beezee Nov 21, 2025
d742bd6
fix: update tooltip placement for user item component
d-beezee Nov 21, 2025
d556e37
fix: add size prop to WatcherList component for consistent button sizing
d-beezee Nov 21, 2025
1546356
fix: update translation keys and improve layout in CommentsUpdatesPan…
iDome89 Nov 21, 2025
26b8bca
fix: update translation keys and improve layout in CommentsUpdatesPan…
iDome89 Nov 24, 2025
4e24eca
fix: update translation keys and improve layout in CommentsUpdatesPan…
iDome89 Nov 24, 2025
6a19308
fix: update translation keys and improve layout in CommentsUpdatesPan…
iDome89 Nov 24, 2025
3bfea08
Merge pull request #1480 from AppQuality/UN-2106
d-beezee Nov 25, 2025
809e102
fix: update unfollow functionality and adjust campaign links in Follo…
d-beezee Nov 25, 2025
0d5b544
fix: update translation keys and improve layout in CommentsUpdatesPan…
iDome89 Nov 26, 2025
bb8d99b
Merge pull request #1479 from AppQuality/UN-2060
d-beezee Nov 28, 2025
54b045f
fix: update translation keys and improve layout in CommentsUpdatesPan…
iDome89 Nov 27, 2025
93a40d1
refactor: extract useAvailableUsers hook for better user management
d-beezee Nov 28, 2025
be69bac
Merge pull request #1484 from AppQuality/UN-2149
d-beezee Nov 28, 2025
fc947d3
refactor: replace hasWorkspaceAccess with availableUsers check in Wat…
d-beezee Dec 1, 2025
1fef7d7
refactor: remove unused useHasWorkspaceAccess function and related im…
d-beezee Dec 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
278 changes: 278 additions & 0 deletions src/assets/empty-monitoring.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/assets/icons/chevron-up-stroke.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
48 changes: 48 additions & 0 deletions src/common/components/WatcherList/MemberAddAutoComplete.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {
Autocomplete,
DropdownFieldNew,
MD,
} from '@appquality/unguess-design-system';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';

const MemberAddAutocomplete = ({
onSelect,
users,
isLoading,
}: {
onSelect: (selectionValue: number) => void;
isLoading: boolean;
users: Array<{ id: number; name: string; email: string }>;
}) => {
const { t } = useTranslation();
const [inputValue, setInputValue] = useState('');

if (!users || isLoading) return null;

return (
<DropdownFieldNew>
<Autocomplete
onInputChange={(value) => setInputValue(value)}
inputValue={inputValue}
selectionValue={null}
placeholder={t(
'__PLAN_PAGE_WATCHER_LIST_SELECT_ADD_MEMBERS_PLACEHOLDER'
)}
options={users.map((user) => ({
children: <MD isBold>{user.name}</MD>,
meta: user.email,
id: `user-${user.id}`,
label: `${user.name} - ${user.email}`,
value: `${user.id}`,
}))}
onOptionClick={({ selectionValue }) => {
setInputValue('');
onSelect(Number(selectionValue));
}}
/>
</DropdownFieldNew>
);
};

export { MemberAddAutocomplete };
129 changes: 129 additions & 0 deletions src/common/components/WatcherList/UserItemComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import {
Avatar,
Ellipsis,
getColor,
IconButton,
MD,
SM,
Tooltip,
} from '@appquality/unguess-design-system';
import { t } from 'i18next';
import { appTheme } from 'src/app/theme';
import { ReactComponent as XStroke } from 'src/assets/icons/x-stroke.svg';
import { getInitials } from 'src/common/components/navigation/header/utils';
import { prepareGravatar } from 'src/common/utils';
import styled from 'styled-components';

const StyledEllipsis = styled(Ellipsis)``;

const UserListItem = styled.div`
display: flex;
padding: ${({ theme }) => `${theme.space.xs} 0`};
align-items: center;
gap: ${({ theme }) => theme.space.sm};

${StyledEllipsis} {
width: 250px;
}
`;

const UserAvatar = ({
image,
name,
isInternal,
}: {
image?: string;
name: string;
isInternal: boolean;
}) => {
if (isInternal) {
return <Avatar avatarType="system" />;
}
return (
<Avatar avatarType={image ? 'image' : 'text'}>
{image ? prepareGravatar(image, 64) : getInitials(name)}
</Avatar>
);
};

const UserItem = ({
onClick,
hideRemoveButton,
isLastOne,
user,
}: {
onClick: () => void;
hideRemoveButton?: boolean;
isLastOne: boolean;
user: {
id: number;
name: string;
email: string;
image?: string;
isInternal: boolean;
isMe?: boolean;
};
}) => {
const { isMe } = user;

const iconButton = (
<IconButton disabled={isLastOne} onClick={onClick}>
<XStroke />
</IconButton>
);

return (
<UserListItem>
<div style={{ paddingLeft: '2px' }}>
<UserAvatar
image={user.image}
name={user.name.length ? user.name : user.email}
isInternal={user.isInternal}
/>
</div>
<div>
<MD
isBold
style={{
color: getColor(appTheme.colors.primaryHue, 600),
}}
>
<StyledEllipsis>
{user.name.length ? user.name : user.email}{' '}
{isMe && t('__WORKSPACE_SETTINGS_CURRENT_MEMBER_YOU_LABEL')}
</StyledEllipsis>
</MD>
{user.name.length > 0 && (
<SM
style={{
color: appTheme.palette.grey[700],
}}
>
<StyledEllipsis>{user.email}</StyledEllipsis>
</SM>
)}
</div>
<div>
{!hideRemoveButton && (
<Tooltip
placement="start"
type="light"
size="large"
content={
isLastOne
? t(
'__PLAN_PAGE_WATCHER_LIST_MODAL_REMOVE_BUTTON_DISABLED_TOOLTIP'
)
: t('__PLAN_PAGE_WATCHER_LIST_MODAL_REMOVE_BUTTON_TOOLTIP')
}
>
{/* the following div is necessary to make Tooltip work with disabled IconButton */}
<div>{iconButton}</div>
</Tooltip>
)}
</div>
</UserListItem>
);
};

export { UserItem };
75 changes: 75 additions & 0 deletions src/common/components/WatcherList/WatchButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Button, Tooltip } from '@appquality/unguess-design-system';
import { useTranslation } from 'react-i18next';
import { appTheme } from 'src/app/theme';

import { ReactComponent as EyeIconFill } from 'src/assets/icons/eye-icon-fill.svg';
import { ReactComponent as EyeIconSlash } from 'src/assets/icons/eye-icon-slash.svg';
import { useGetUsersMeQuery } from 'src/features/api';

const WatchButton = ({
isWatching,
isLastOne,
onClick,
isDisabled,
}: {
isWatching: boolean;
isDisabled: boolean;
isLastOne: boolean;
onClick: () => void;
}) => {
const { data: currentUser } = useGetUsersMeQuery();
const { t } = useTranslation();

const isLastWatcher = isWatching && isLastOne;

const iconColor = (() => {
if (isDisabled) return appTheme.palette.grey[400];
if (!isWatching) return '#fff';
return undefined;
})();

const EyeIcon = isWatching ? EyeIconSlash : EyeIconFill;

if (!currentUser) return null;

const button = (
<Button
isStretched
disabled={isDisabled}
isPrimary={!isWatching}
onClick={onClick}
>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<EyeIcon color={iconColor} />

{isWatching
? t('__PLAN_PAGE_WATCHER_LIST_MODAL_UNFOLLOW_BUTTON')
: t('__PLAN_PAGE_WATCHER_LIST_MODAL_FOLLOW_BUTTON')}
</div>
</Button>
);

if (isDisabled) {
return (
<Tooltip
placement="start"
type="light"
size="large"
content={
isLastWatcher
? t(
'__PLAN_PAGE_WATCHER_LIST_MODAL_UNFOLLOW_BUTTON_DISABLED_TOOLTIP'
)
: t('__PLAN_PAGE_WATCHER_LIST_MODAL_FOLLOW_BUTTON_DISABLED_TOOLTIP')
}
>
{/* the following div is necessary to make Tooltip work with disabled Button */}
<div>{button}</div>
</Tooltip>
);
}

return button;
};

export { WatchButton };
Loading
Loading