Compare commits

...

4 Commits

Author SHA1 Message Date
chamikaJ
aa00f6fe21 Merge branches 'feature/guest-user' and 'main' of https://github.com/Worklenz/worklenz into feature/guest-user 2025-07-24 10:19:12 +05:30
chamikaJ
95aa2ef8ee refactor: centralize Ant Design imports and remove unused components
- Replaced direct imports from 'antd' with centralized imports from '@/shared/antd-imports' across multiple components for improved maintainability.
- Removed the TawkTo component as it is no longer needed.
- Updated the CustomAvatar and various other components to utilize the new import structure, enhancing code clarity and consistency.
- Introduced the InviteTeamMembersModal component, streamlining the invitation process for team members.
2025-07-23 17:20:42 +05:30
chamikaJ
e3443eedfb feat(localization): add guest role text and invitation message fields for team members
- Introduced new localization strings for guest role descriptions and invitation messages across German, English, and Spanish.
- Added fields for personal messages and email input instructions in the team members settings, enhancing user experience during team member invitations.
2025-07-23 17:20:30 +05:30
chamikaJ
03bd3659e0 feat(guidelines): add general coding, localization, and component naming rules
- Introduced a comprehensive set of general coding guidelines to enhance code readability and maintainability.
- Established a localization rule to prevent hard-coded user-facing text, ensuring all strings are managed through the localization system.
- Defined a naming convention for React components, mandating the use of PascalCase for consistency across the codebase.
- Included examples and enforcement strategies for each rule to facilitate adherence during development and code reviews.
2025-07-23 17:20:14 +05:30
23 changed files with 608 additions and 150 deletions

View File

@@ -0,0 +1,131 @@
# General Coding Guidelines
## Rule Summary
Follow these rules when you write code:
1. **Use Early Returns**
- Prefer early returns and guard clauses to reduce nesting and improve readability, especially for error handling.
2. **Tailwind for Styling**
- Always use Tailwind CSS utility classes for styling HTML elements.
- Avoid writing custom CSS or using inline `style` tags.
3. **Class Tag Syntax**
- Use `class:` directive (e.g., `class:active={isActive}`) instead of the ternary operator in class tags whenever possible.
4. **Descriptive Naming**
- Use clear, descriptive names for variables, functions, and constants.
- Use auxiliary verbs for booleans and state (e.g., `isLoaded`, `hasError`, `shouldRender`).
- Event handler functions should be prefixed with `handle`, e.g., `handleClick` for `onClick`, `handleKeyDown` for `onKeyDown`.
5. **Naming Conventions**
- **Directories:** Use lowercase with dashes (e.g., `components/auth-wizard`).
- **Variables & Functions:** Use `camelCase` (e.g., `userList`, `fetchData`).
- **Types & Interfaces:** Use `PascalCase` (e.g., `User`, `ButtonProps`).
- **Exports:** Favor named exports for components.
- **No Unused Variables:** Remove unused variables and imports.
6. **File Layout**
- Order: exported component → subcomponents → hooks/helpers → static content.
7. **Props & Types**
- Define props with TypeScript `interface` or `type`, not `prop-types`.
- Example:
```ts
interface ButtonProps {
label: string;
onClick?: () => void;
}
export function Button({ label, onClick }: ButtonProps) {
return <button onClick={onClick}>{label}</button>;
}
```
8. **Component Declaration**
- Use the `function` keyword for components, not arrow functions.
9. **Hooks Usage**
- Call hooks (e.g., `useState`, `useEffect`) only at the top level of components.
- Extract reusable logic into custom hooks (e.g., `useAuth`, `useFormValidation`).
10. **Memoization & Performance**
- Use `React.memo`, `useCallback`, and `useMemo` where appropriate.
- Avoid inline functions in JSX—pull handlers out or wrap in `useCallback`.
11. **Composition**
- Favor composition (render props, `children`) over inheritance.
12. **Code Splitting**
- Use `React.lazy` + `Suspense` for code splitting.
13. **Refs**
- Use refs only for direct DOM access.
14. **Forms**
- Prefer controlled components for forms.
15. **Error Boundaries**
- Implement an error boundary component for catching render errors.
16. **Effect Cleanup**
- Clean up effects in `useEffect` to prevent memory leaks.
17. **Accessibility**
- Apply appropriate ARIA attributes to interactive elements.
- For example, an `<a>` tag should have `tabindex="0"`, `aria-label`, `onClick`, and `onKeyDown` attributes as appropriate.
## Examples
### ✅ Correct
```tsx
// File: components/user-profile.tsx
interface UserProfileProps {
user: User;
isLoaded: boolean;
hasError: boolean;
}
export function UserProfile({ user, isLoaded, hasError }: UserProfileProps) {
if (!isLoaded) return <div>Loading...</div>;
if (hasError) return <div role="alert">Error loading user.</div>;
const handleClick = useCallback(() => {
// ...
}, [user]);
return (
<button
className="bg-blue-500 text-white"
aria-label="View user profile"
tabIndex={0}
onClick={handleClick}
onKeyDown={handleKeyDown}
>
{user.name}
</button>
);
}
```
### ❌ Incorrect
```tsx
// File: components/UserProfile.jsx
function userprofile(props) {
if (props.isLoaded) {
// ...
}
}
return (
<button style={{ color: 'white' }} onClick={() => doSomething()}>
View
</button>
);
```
## Enforcement
- All new code must follow these guidelines.
- Code reviews should reject code that does not comply with these rules.
- Refactor existing code to follow these guidelines when making changes.

View File

@@ -0,0 +1,38 @@
# Localization Rule: No Hard-Coded User-Facing Text
## Rule
- All user-facing text **must** be added to the localization system at `@/locales`.
- **Never** hard-code user-facing strings directly in components, pages, or business logic.
- Use the appropriate i18n or localization utility to fetch and display all text.
- **Always** provide a `defaultValue` when using the `t()` function for translations, e.g., `{t('emailPlaceholder', {defaultValue: 'Enter your email'})}`.
## Rationale
- Ensures the application is fully translatable and accessible to all supported languages.
- Prevents missed strings during translation updates.
- Promotes consistency and maintainability.
- Providing a `defaultValue` ensures a fallback is shown if the translation key is missing.
## Examples
### ✅ Correct
```tsx
import { useTranslation } from 'react-i18next';
const { t } = useTranslation();
return <input placeholder={t('emailPlaceholder', { defaultValue: 'Enter your email' })} />;
```
### ❌ Incorrect
```tsx
return <input placeholder={t('emailPlaceholder')} />;
// or
return <input placeholder="Enter your email" />;
```
## Enforcement
- All new user-facing text **must** be added to the appropriate file in `@/locales`.
- Every use of `t()` **must** include a `defaultValue` for fallback.
- Code reviews should reject any hard-coded user-facing strings or missing `defaultValue` in translations.
- Refactor existing hard-coded text to use the localization system and add `defaultValue` when modifying related code.

View File

@@ -0,0 +1,39 @@
# React Component Naming Rule: PascalCase
## Rule
- All React component names **must** use PascalCase.
- This applies to:
- Component file names (e.g., `MyComponent.tsx`, `UserProfile.jsx`)
- Exported component identifiers (e.g., `export const MyComponent = ...` or `function UserProfile() { ... }`)
## Rationale
- PascalCase is the community standard for React components.
- Ensures consistency and readability across the codebase.
- Prevents confusion between components and regular functions/variables.
## Examples
### ✅ Correct
```tsx
// File: UserProfile.tsx
export function UserProfile() { ... }
// File: TaskList.tsx
const TaskList = () => { ... }
export default TaskList;
```
### ❌ Incorrect
```tsx
// File: userprofile.tsx
export function userprofile() { ... }
// File: task-list.jsx
const task_list = () => { ... }
export default task_list;
```
## Enforcement
- All new React components **must** follow this rule.
- Refactor existing components to PascalCase when modifying or moving them.
- Code reviews should reject non-PascalCase component names.

View File

@@ -38,10 +38,16 @@
"updateMemberErrorMessage": "Aktualisierung des Teammitglieds fehlgeschlagen. Bitte versuchen Sie es erneut.",
"memberText": "Mitglied",
"adminText": "Administrator",
"guestText": "Gast (Nur Lesen)",
"ownerText": "Team-Besitzer",
"addedText": "Hinzugefügt",
"updatedText": "Aktualisiert",
"noResultFound": "Geben Sie eine E-Mail-Adresse ein und drücken Sie Enter...",
"jobTitlesFetchError": "Fehler beim Abrufen der Jobtitel",
"invitationResent": "Einladung erfolgreich erneut gesendet!"
"invitationResent": "Einladung erfolgreich erneut gesendet!",
"emailsStepDescription": "Geben Sie E-Mail-Adressen für Teammitglieder ein, die Sie einladen möchten",
"personalMessageLabel": "Persönliche Nachricht",
"personalMessagePlaceholder": "Fügen Sie eine persönliche Nachricht zu Ihrer Einladung hinzu (optional)",
"optionalFieldLabel": "(Optional)",
"inviteTeamMembersModalTitle": "Teammitglieder einladen"
}

View File

@@ -38,10 +38,16 @@
"updateMemberErrorMessage": "Failed to update team member. Please try again.",
"memberText": "Member",
"adminText": "Admin",
"guestText": "Guest (Read-only)",
"ownerText": "Team Owner",
"addedText": "Added",
"updatedText": "Updated",
"noResultFound": "Type an email address and hit enter...",
"jobTitlesFetchError": "Failed to fetch job titles",
"invitationResent": "Invitation resent successfully!"
"invitationResent": "Invitation resent successfully!",
"emailsStepDescription": "Enter email addresses for team members you'd like to invite",
"personalMessageLabel": "Personal Message",
"personalMessagePlaceholder": "Add a personal message to your invitation (optional)",
"optionalFieldLabel": "(Optional)",
"inviteTeamMembersModalTitle": "Invite team members"
}

View File

@@ -38,10 +38,16 @@
"updateMemberErrorMessage": "Error al actualizar miembro del equipo. Por favor, intente nuevamente.",
"memberText": "Miembro del equipo",
"adminText": "Administrador",
"guestText": "Invitado (Solo lectura)",
"ownerText": "Propietario del equipo",
"addedText": "Agregado",
"updatedText": "Actualizado",
"noResultFound": "Escriba una dirección de correo electrónico y presione enter...",
"jobTitlesFetchError": "Error al obtener los cargos",
"invitationResent": "¡Invitación reenviada exitosamente!"
"invitationResent": "¡Invitación reenviada exitosamente!",
"emailsStepDescription": "Ingrese las direcciones de correo de los miembros del equipo que desea invitar",
"personalMessageLabel": "Mensaje Personal",
"personalMessagePlaceholder": "Agregue un mensaje personal a su invitación (opcional)",
"optionalFieldLabel": "(Opcional)",
"inviteTeamMembersModalTitle": "Invitar miembros del equipo"
}

View File

@@ -1,8 +1,6 @@
import React from 'react';
import Tooltip from 'antd/es/tooltip';
import Avatar from 'antd/es/avatar';
import { AvatarNamesMap } from '../shared/constants';
import { Avatar, Tooltip } from '@/shared/antd-imports';
interface CustomAvatarProps {
avatarName: string;

View File

@@ -1,50 +0,0 @@
import { useEffect } from 'react';
// Add TypeScript declarations for Tawk_API
declare global {
interface Window {
Tawk_API?: any;
Tawk_LoadStart?: Date;
}
}
interface TawkToProps {
propertyId: string;
widgetId: string;
}
const TawkTo: React.FC<TawkToProps> = ({ propertyId, widgetId }) => {
useEffect(() => {
// Initialize tawk.to chat
const s1 = document.createElement('script');
s1.async = true;
s1.src = `https://embed.tawk.to/${propertyId}/${widgetId}`;
s1.setAttribute('crossorigin', '*');
const s0 = document.getElementsByTagName('script')[0];
s0.parentNode?.insertBefore(s1, s0);
return () => {
// Clean up when the component unmounts
// Remove the script tag
const tawkScript = document.querySelector(`script[src*="tawk.to/${propertyId}"]`);
if (tawkScript && tawkScript.parentNode) {
tawkScript.parentNode.removeChild(tawkScript);
}
// Remove the tawk.to iframe
const tawkIframe = document.getElementById('tawk-iframe');
if (tawkIframe) {
tawkIframe.remove();
}
// Reset Tawk globals
delete window.Tawk_API;
delete window.Tawk_LoadStart;
};
}, [propertyId, widgetId]);
return null;
};
export default TawkTo;

View File

@@ -1,11 +1,8 @@
import { adminCenterApiService } from '@/api/admin-center/admin-center.api.service';
import { fetchStorageInfo } from '@/features/admin-center/admin-center.slice';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { useAppSelector } from '@/hooks/useAppSelector';
import { SUBSCRIPTION_STATUS } from '@/shared/constants';
import { IBillingAccountStorage } from '@/types/admin-center/admin-center.types';
import logger from '@/utils/errorLogger';
import { Card, Progress, Typography } from 'antd/es';
import { Card, Progress, Typography } from '@/shared/antd-imports';
import { useEffect, useState, useMemo } from 'react';
import { useTranslation } from 'react-i18next';

View File

@@ -1,8 +1,7 @@
import { adminCenterApiService } from '@/api/admin-center/admin-center.api.service';
import logger from '@/utils/errorLogger';
import { EnterOutlined, EditOutlined } from '@/shared/antd-imports';
import { Card, Button, Tooltip, Typography } from '@/shared/antd-imports';
import TextArea from 'antd/es/input/TextArea';
import { Card, Button, Tooltip, Typography, TextArea } from '@/shared/antd-imports';
import { TFunction } from 'i18next';
import { useState, useEffect } from 'react';

View File

@@ -1,31 +1,32 @@
import { InputRef } from 'antd/es/input';
import Card from 'antd/es/card';
import Checkbox from 'antd/es/checkbox';
import Divider from 'antd/es/divider';
import Dropdown from 'antd/es/dropdown';
import Empty from 'antd/es/empty';
import Flex from 'antd/es/flex';
import Input from 'antd/es/input';
import List from 'antd/es/list';
import Typography from 'antd/es/typography';
import Button from 'antd/es/button';
import { useMemo, useRef, useState } from 'react';
import {
InputRef,
PlusOutlined,
UsergroupAddOutlined,
Card,
Flex,
Input,
List,
Typography,
Checkbox,
Divider,
Button,
Empty,
Dropdown,
CheckboxChangeEvent,
} from '@/shared/antd-imports';
import { useAppSelector } from '@/hooks/useAppSelector';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { toggleProjectMemberDrawer } from '../../../features/projects/singleProject/members/projectMembersSlice';
import { colors } from '../../../styles/colors';
import { PlusOutlined, UsergroupAddOutlined } from '@/shared/antd-imports';
import { toggleProjectMemberDrawer } from '@features/projects/singleProject/members/projectMembersSlice';
import { colors } from '@/styles/colors';
import { useTranslation } from 'react-i18next';
import SingleAvatar from '@/components/common/single-avatar/single-avatar';
import { CheckboxChangeEvent } from 'antd/es/checkbox';
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
import { ITeamMembersViewModel } from '@/types/teamMembers/teamMembersViewModel.types';
import { sortByBooleanField, sortBySelection, sortTeamMembers } from '@/utils/sort-team-members';
import { sortTeamMembers } from '@/utils/sort-team-members';
import { useAuthService } from '@/hooks/useAuth';
import { useSocket } from '@/socket/socketContext';
import { SocketEvents } from '@/shared/socket-events';
import { getTeamMembers } from '@/features/team-members/team-members.slice';
interface BoardAssigneeSelectorProps {
task: IProjectTask;

View File

@@ -0,0 +1,280 @@
import { Button, Flex, Input, message, Modal, Select, Space, Typography, List, Avatar } from '@/shared/antd-imports';
import { useAppSelector } from '@/hooks/useAppSelector';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import {
toggleInviteMemberDrawer,
triggerTeamMembersRefresh,
} from '../../../features/settings/member/memberSlice';
import { useTranslation } from 'react-i18next';
import { useState, useEffect, useCallback, useMemo, memo } from 'react';
import { ITeamMemberCreateRequest } from '@/types/teamMembers/team-member-create-request';
import { DeleteOutlined, UserOutlined } from '@ant-design/icons';
interface MemberEntry {
email: string;
access: 'member' | 'admin' | 'guest';
}
const InviteTeamMembersModal = () => {
const [newMembers, setNewMembers] = useState<MemberEntry[]>([]);
const [emailInput, setEmailInput] = useState('');
const [loading, setLoading] = useState(false);
const { t } = useTranslation('settings/team-members');
const isModalOpen = useAppSelector(state => state.memberReducer.isInviteMemberDrawerOpen);
const dispatch = useAppDispatch();
useEffect(() => {
if (isModalOpen) {
// Reset state when modal opens
setNewMembers([]);
setEmailInput('');
// Focus on email input when modal opens
setTimeout(() => {
const emailInput = document.querySelector('input[type="text"]');
if (emailInput) {
(emailInput as HTMLElement).focus();
}
}, 100);
}
}, [isModalOpen]);
const handleFormSubmit = async () => {
try {
setLoading(true);
if (newMembers.length === 0) {
message.error('Please add at least one member');
return;
}
// Send invitations for each member
const promises = newMembers.map(member => {
const body: ITeamMemberCreateRequest = {
emails: [member.email],
is_admin: member.access === 'admin',
is_guest: member.access === 'guest',
};
return teamMembersApiService.createTeamMember(body);
});
const results = await Promise.allSettled(promises);
const successful = results.filter(r => r.status === 'fulfilled').length;
if (successful > 0) {
message.success(`${successful} invitation(s) sent successfully`);
setNewMembers([]);
setEmailInput('');
dispatch(triggerTeamMembersRefresh());
dispatch(toggleInviteMemberDrawer());
}
const failed = results.length - successful;
if (failed > 0) {
message.error(`${failed} invitation(s) failed`);
}
} catch (error) {
message.error(t('createMemberErrorMessage'));
} finally {
setLoading(false);
}
};
const handleClose = () => {
setNewMembers([]);
setEmailInput('');
dispatch(toggleInviteMemberDrawer());
};
const handleEmailKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault();
const trimmedEmail = emailInput.trim();
// Don't show error for empty input, just ignore
if (!trimmedEmail) {
return;
}
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailPattern.test(trimmedEmail)) {
message.error('Please enter a valid email address');
return;
}
// Check if email already exists
if (newMembers.find(m => m.email === trimmedEmail)) {
message.warning('Email already added');
return;
}
// Add new member
setNewMembers([...newMembers, { email: trimmedEmail, access: 'member' }]);
setEmailInput('');
}
};
const updateMemberAccess = (index: number, access: 'member' | 'admin' | 'guest') => {
const updated = [...newMembers];
updated[index].access = access;
setNewMembers(updated);
};
const removeMember = (index: number) => {
setNewMembers(newMembers.filter((_, i) => i !== index));
};
const accessOptions = useMemo(() => [
{ value: 'member', label: t('memberText') },
{ value: 'admin', label: t('adminText') },
{ value: 'guest', label: t('guestText') },
], [t]);
const renderContent = () => (
<div>
<div style={{ marginBottom: 16 }}>
<Input
placeholder="Enter email address and press Enter to add"
value={emailInput}
onChange={(e) => setEmailInput(e.target.value)}
onKeyPress={handleEmailKeyPress}
size="middle"
autoFocus
style={{
borderRadius: 8,
fontSize: 14
}}
/>
<Typography.Text type="secondary" style={{ fontSize: 12, marginTop: 6, display: 'block', fontStyle: 'italic' }}>
Press Enter to add Multiple emails can be added
</Typography.Text>
</div>
{newMembers.length > 0 && (
<div style={{ marginBottom: 20 }}>
<Typography.Text type="secondary" style={{ fontSize: 13, marginBottom: 12, display: 'block', fontWeight: 500 }}>
Members to invite ({newMembers.length})
</Typography.Text>
<div style={{
background: 'rgba(0, 0, 0, 0.02)',
borderRadius: 8,
padding: '8px 12px',
border: '1px solid rgba(0, 0, 0, 0.06)'
}}>
<List
size="small"
dataSource={newMembers}
split={false}
renderItem={(member, index) => (
<List.Item
style={{
padding: '8px 0',
borderRadius: 6,
marginBottom: index < newMembers.length - 1 ? 4 : 0
}}
actions={[
<Select
size="small"
value={member.access}
onChange={(value) => updateMemberAccess(index, value)}
options={accessOptions}
style={{ width: 90 }}
variant="outlined"
/>,
<Button
type="text"
icon={<DeleteOutlined />}
onClick={() => removeMember(index)}
size="small"
danger
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
/>
]}
>
<List.Item.Meta
avatar={
<Avatar size={32} style={{
backgroundColor: '#1677ff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<UserOutlined style={{ fontSize: 14 }} />
</Avatar>
}
title={
<span style={{
fontSize: 14,
fontWeight: 500,
color: 'rgba(0, 0, 0, 0.88)'
}}>
{member.email}
</span>
}
description={
<span style={{
fontSize: 12,
color: '#52c41a',
fontWeight: 500
}}>
Ready to invite
</span>
}
/>
</List.Item>
)}
/>
</div>
</div>
)}
</div>
);
return (
<Modal
title={
<Typography.Text strong style={{ fontSize: 18 }}>
{t('inviteTeamMembersModalTitle')}
</Typography.Text>
}
open={isModalOpen}
onCancel={handleClose}
width={520}
destroyOnClose
bodyStyle={{ padding: '16px 20px' }}
footer={
<Flex justify="end">
<Space>
<Button onClick={handleClose}>
Cancel
</Button>
<Button
type="primary"
onClick={handleFormSubmit}
loading={loading}
disabled={newMembers.length === 0}
>
Send
</Button>
</Space>
</Flex>
}
>
{renderContent()}
</Modal>
);
};
export default memo(InviteTeamMembersModal);

View File

@@ -0,0 +1 @@
export { default } from './InviteTeamMembersModal';

View File

@@ -1,26 +1,32 @@
import React, { useState, useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { Card, Empty } from '@/shared/antd-imports';
import { useSelector, useDispatch } from 'react-redux';
import { RootState } from '@/app/store';
import '../EnhancedKanbanBoard.css';
import '../EnhancedKanbanGroup.css';
import '../EnhancedKanbanTaskCard.css';
import ImprovedTaskFilters from '../../task-management/improved-task-filters';
import Card from 'antd/es/card';
import Spin from 'antd/es/spin';
import Empty from 'antd/es/empty';
import { reorderGroups, reorderEnhancedKanbanGroups, reorderTasks, reorderEnhancedKanbanTasks, fetchEnhancedKanbanLabels, fetchEnhancedKanbanGroups, fetchEnhancedKanbanTaskAssignees } from '@/features/enhanced-kanban/enhanced-kanban.slice';
import { fetchStatusesCategories } from '@/features/taskAttributes/taskStatusSlice';
import { useAppSelector } from '@/hooks/useAppSelector';
import KanbanGroup from './KanbanGroup';
import EnhancedKanbanCreateSection from '../EnhancedKanbanCreateSection';
import { useSocket } from '@/socket/socketContext';
import { SocketEvents } from '@/shared/socket-events';
import { useAuthService } from '@/hooks/useAuth';
import { useSocket } from '@/socket/socketContext';
import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers';
import { statusApiService } from '@/api/taskAttributes/status/status.api.service';
import alertService from '@/services/alerts/alertService';
import logger from '@/utils/errorLogger';
import {
reorderGroups,
reorderEnhancedKanbanGroups,
reorderTasks,
reorderEnhancedKanbanTasks,
fetchEnhancedKanbanLabels,
fetchEnhancedKanbanGroups,
fetchEnhancedKanbanTaskAssignees,
} from '@/features/enhanced-kanban/enhanced-kanban.slice';
import { fetchStatusesCategories } from '@/features/taskAttributes/taskStatusSlice';
import { checkTaskDependencyStatus } from '@/utils/check-task-dependency-status';
import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers';
import KanbanGroup from './KanbanGroup';
import EnhancedKanbanCreateSection from '../EnhancedKanbanCreateSection';
import ImprovedTaskFilters from '../../task-management/improved-task-filters';
import { SocketEvents } from '@/shared/socket-events';
import '../EnhancedKanbanBoard.css';
import '../EnhancedKanbanGroup.css';
import '../EnhancedKanbanTaskCard.css';
const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ projectId }) => {
const dispatch = useDispatch();

View File

@@ -1,37 +1,38 @@
import React, { useCallback, useMemo, useState } from 'react';
import {
ForkOutlined,
CaretDownFilled,
CaretRightFilled,
Tag,
Flex,
Tooltip,
Progress,
Typography,
Button,
Divider,
List,
Skeleton,
PlusOutlined,
Dayjs,
dayjs,
} from '@/shared/antd-imports';
import { useSortable, defaultAnimateLayoutChanges } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
import { useTranslation } from 'react-i18next';
import { useAppSelector } from '@/hooks/useAppSelector';
import './EnhancedKanbanTaskCard.css';
import Flex from 'antd/es/flex';
import Tag from 'antd/es/tag';
import Tooltip from 'antd/es/tooltip';
import Progress from 'antd/es/progress';
import Button from 'antd/es/button';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { setShowTaskDrawer, setSelectedTaskId } from '@/features/task-drawer/task-drawer.slice';
import PrioritySection from '../board/taskCard/priority-section/priority-section';
import Typography from 'antd/es/typography';
import CustomDueDatePicker from '../board/custom-due-date-picker';
import { themeWiseColor } from '@/utils/themeWiseColor';
import { ForkOutlined } from '@/shared/antd-imports';
import { Dayjs } from 'dayjs';
import dayjs from 'dayjs';
import { CaretDownFilled, CaretRightFilled } from '@/shared/antd-imports';
import {
fetchBoardSubTasks,
toggleTaskExpansion,
} from '@/features/enhanced-kanban/enhanced-kanban.slice';
import { Divider } from '@/shared/antd-imports';
import { List } from '@/shared/antd-imports';
import { Skeleton } from '@/shared/antd-imports';
import { PlusOutlined } from '@/shared/antd-imports';
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
import { themeWiseColor } from '@/utils/themeWiseColor';
import './EnhancedKanbanTaskCard.css';
import LazyAssigneeSelectorWrapper from '../task-management/lazy-assignee-selector';
import CustomDueDatePicker from '../board/custom-due-date-picker';
import BoardSubTaskCard from '@/pages/projects/projectView/board/board-section/board-sub-task-card/board-sub-task-card';
import BoardCreateSubtaskCard from '@/pages/projects/projectView/board/board-section/board-sub-task-card/board-create-sub-task-card';
import { useTranslation } from 'react-i18next';
import EnhancedKanbanCreateSubtaskCard from './EnhancedKanbanCreateSubtaskCard';
import LazyAssigneeSelectorWrapper from '@/components/task-management/lazy-assignee-selector';
import AvatarGroup from '@/components/AvatarGroup';
interface EnhancedKanbanTaskCardProps {

View File

@@ -1,6 +1,4 @@
import { SettingOutlined } from '@/shared/antd-imports';
import Tooltip from 'antd/es/tooltip';
import Button from 'antd/es/button';
import { SettingOutlined, Tooltip, Button } from '@/shared/antd-imports';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { toggleDrawer } from '../../../features/projects/status/StatusSlice';
import { colors } from '@/styles/colors';

View File

@@ -1,20 +1,16 @@
import React, { useCallback, useState } from 'react';
import React, { useCallback } from 'react';
import { Form, Input, Select, Button, Drawer, Flex, Badge } from '@/shared/antd-imports';
import { useTranslation } from 'react-i18next';
import Flex from 'antd/es/flex';
import Badge from 'antd/es/badge';
import Drawer from 'antd/es/drawer';
import Form from 'antd/es/form';
import Input from 'antd/es/input';
import Select from 'antd/es/select';
import Button from 'antd/es/button/button';
import { useAppSelector } from '@/hooks/useAppSelector';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { toggleDrawer } from '@/features/projects/status/StatusSlice';
import './create-status-drawer.css';
import { createStatus, fetchStatusesCategories, fetchStatuses } from '@/features/taskAttributes/taskStatusSlice';
import {
createStatus,
fetchStatusesCategories,
fetchStatuses,
} from '@/features/taskAttributes/taskStatusSlice';
import { ITaskStatusCategory } from '@/types/status.types';
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
import useTabSearchParam from '@/hooks/useTabSearchParam';
@@ -22,6 +18,8 @@ import { evt_project_board_create_status } from '@/shared/worklenz-analytics-eve
import { fetchTaskGroups } from '@/features/tasks/tasks.slice';
import { fetchBoardTaskGroups } from '@/features/board/board-slice';
import './create-status-drawer.css';
const StatusDrawer: React.FC = () => {
const dispatch = useAppDispatch();
const { trackMixpanelEvent } = useMixpanelTracking();

View File

@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Drawer, Alert, Card, Select, Button, Typography, Badge, Form } from '@/shared/antd-imports';
import { useTranslation } from 'react-i18next';
import Form from 'antd/es/form';
import { useAppSelector } from '@/hooks/useAppSelector';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { fetchStatuses, fetchStatusesCategories } from '@/features/taskAttributes/taskStatusSlice';
@@ -9,9 +9,7 @@ import useTabSearchParam from '@/hooks/useTabSearchParam';
import { fetchTaskGroups } from '@/features/tasks/tasks.slice';
import { deleteStatusToggleDrawer } from '@/features/projects/status/DeleteStatusSlice';
import { Drawer, Alert, Card, Select, Button, Typography, Badge } from '@/shared/antd-imports';
import { DownOutlined } from '@/shared/antd-imports';
import { useSelector } from 'react-redux';
import {
deleteSection,
IGroupBy,
@@ -21,7 +19,6 @@ import { phasesApiService } from '@/api/taskAttributes/phases/phases.api.service
import logger from '@/utils/errorLogger';
import { fetchEnhancedKanbanGroups } from '@/features/enhanced-kanban/enhanced-kanban.slice';
const { Title, Text } = Typography;
const { Option } = Select;
const DeleteStatusDrawer: React.FC = () => {
const [currentStatus, setCurrentStatus] = useState<string>('');

View File

@@ -1,7 +1,6 @@
import { useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { CaretDownFilled } from '@/shared/antd-imports';
import { ConfigProvider, Flex, Dropdown, Button } from 'antd/es';
import { CaretDownFilled, Dropdown, Button, Flex, ConfigProvider } from '@/shared/antd-imports';
import { useSearchParams } from 'react-router-dom';
import ConfigPhaseButton from '@features/projects/singleProject/phase/ConfigPhaseButton';

View File

@@ -1,14 +1,16 @@
import { CaretDownFilled } from '@/shared/antd-imports';
import Badge from 'antd/es/badge';
import Button from 'antd/es/button';
import Card from 'antd/es/card';
import Checkbox from 'antd/es/checkbox';
import Dropdown from 'antd/es/dropdown';
import Empty from 'antd/es/empty';
import Flex from 'antd/es/flex';
import Input, { InputRef } from 'antd/es/input';
import List from 'antd/es/list';
import Space from 'antd/es/space';
import {
CaretDownFilled,
Card,
Flex,
Input,
List,
Checkbox,
Dropdown,
Button,
Empty,
Space,
InputRef,
} from '@/shared/antd-imports';
import { useSearchParams } from 'react-router-dom';
import { useMemo, useRef, useState, useEffect } from 'react';

View File

@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import { Col, ConfigProvider, Flex, Menu, MenuProps, Alert } from '@/shared/antd-imports';
import { createPortal } from 'react-dom';
import InviteTeamMembers from '../../components/common/invite-team-members/invite-team-members';
import InviteTeamMembersModal from '../../components/common/invite-team-members-modal';
import InviteButton from './invite/InviteButton';
import MobileMenuButton from './mobileMenu/MobileMenuButton';
import NavbarLogo from './navbar-logo';
@@ -179,7 +179,7 @@ const Navbar = () => {
</Flex>
</Flex>
{isOwnerOrAdmin && createPortal(<InviteTeamMembers />, document.body, 'invite-team-members')}
{isOwnerOrAdmin && createPortal(<InviteTeamMembersModal />, document.body, 'invite-team-members-modal')}
{createPortal(<NotificationDrawer />, document.body, 'notification-drawer')}
</Col>
);

View File

@@ -67,6 +67,8 @@ import {
Radio,
} from 'antd/es';
import TextArea from 'antd/es/input/TextArea';
// Icons - Import commonly used ones
export {
EditOutlined,
@@ -240,6 +242,7 @@ export {
Timeline,
Mentions,
Radio,
TextArea
};
// TypeScript Types - Import commonly used ones
@@ -263,6 +266,7 @@ export type {
PaginationProps,
CollapseProps,
TablePaginationConfig,
CheckboxChangeEvent
} from 'antd/es';
// Dayjs

View File

@@ -4,4 +4,5 @@ export interface ITeamMemberCreateRequest extends ITeamMember {
job_title?: string | null;
emails?: string | string[];
is_admin?: boolean;
is_guest?: boolean;
}