Compare commits
2 Commits
feature/gu
...
developmen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d17490f7e | ||
|
|
8830af2cbb |
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"permissions": {
|
|
||||||
"allow": [
|
|
||||||
"Bash(find:*)",
|
|
||||||
"Bash(npm run build:*)",
|
|
||||||
"Bash(npm run type-check:*)",
|
|
||||||
"Bash(npm run:*)"
|
|
||||||
],
|
|
||||||
"deny": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
---
|
|
||||||
alwaysApply: true
|
|
||||||
---
|
|
||||||
# Ant Design Import Rules for Worklenz
|
|
||||||
|
|
||||||
## 🚨 CRITICAL: Always Use Centralized Imports
|
|
||||||
|
|
||||||
**NEVER import Ant Design components directly from 'antd' or '@ant-design/icons'**
|
|
||||||
|
|
||||||
### ✅ Correct Import Pattern
|
|
||||||
```typescript
|
|
||||||
import { Button, Input, Select, EditOutlined, PlusOutlined } from '@antd-imports';
|
|
||||||
// or
|
|
||||||
import { Button, Input, Select, EditOutlined, PlusOutlined } from '@/shared/antd-imports';
|
|
||||||
```
|
|
||||||
|
|
||||||
### ❌ Forbidden Import Patterns
|
|
||||||
```typescript
|
|
||||||
// NEVER do this:
|
|
||||||
import { Button, Input, Select } from 'antd';
|
|
||||||
import { EditOutlined, PlusOutlined } from '@ant-design/icons';
|
|
||||||
```
|
|
||||||
|
|
||||||
## Why This Rule Exists
|
|
||||||
|
|
||||||
### Benefits of Centralized Imports:
|
|
||||||
- **Better Tree-Shaking**: Optimized bundle size through centralized management
|
|
||||||
- **Consistent React Context**: Proper context sharing across components
|
|
||||||
- **Type Safety**: Centralized TypeScript definitions
|
|
||||||
- **Maintainability**: Single source of truth for all Ant Design imports
|
|
||||||
- **Performance**: Reduced bundle size and improved loading times
|
|
||||||
|
|
||||||
## What's Available in `@antd-imports`
|
|
||||||
|
|
||||||
### Core Components
|
|
||||||
- **Layout**: Layout, Row, Col, Flex, Divider, Space
|
|
||||||
- **Navigation**: Menu, Tabs, Breadcrumb, Pagination
|
|
||||||
- **Data Entry**: Input, Select, DatePicker, TimePicker, Form, Checkbox, InputNumber
|
|
||||||
- **Data Display**: Table, List, Card, Tag, Avatar, Badge, Progress, Statistic
|
|
||||||
- **Feedback**: Modal, Drawer, Alert, Message, Notification, Spin, Skeleton, Result
|
|
||||||
- **Other**: Button, Typography, Tooltip, Popconfirm, Dropdown, ConfigProvider
|
|
||||||
|
|
||||||
### Icons
|
|
||||||
Common icons including: EditOutlined, DeleteOutlined, PlusOutlined, MoreOutlined, CheckOutlined, CloseOutlined, CalendarOutlined, UserOutlined, TeamOutlined, and many more.
|
|
||||||
|
|
||||||
### Utilities
|
|
||||||
- **appMessage**: Centralized message utility
|
|
||||||
- **appNotification**: Centralized notification utility
|
|
||||||
- **antdConfig**: Default Ant Design configuration
|
|
||||||
- **taskManagementAntdConfig**: Task-specific configuration
|
|
||||||
|
|
||||||
## Implementation Guidelines
|
|
||||||
|
|
||||||
### When Creating New Components:
|
|
||||||
1. **Always** import from `@/shared/antd-imports`
|
|
||||||
2. Use `appMessage` and `appNotification` for user feedback
|
|
||||||
3. Apply `antdConfig` for consistent styling
|
|
||||||
4. Use `taskManagementAntdConfig` for task-related components
|
|
||||||
|
|
||||||
### When Refactoring Existing Code:
|
|
||||||
1. Replace direct 'antd' imports with `@/shared/antd-imports`
|
|
||||||
2. Replace direct '@ant-design/icons' imports with `@/shared/antd-imports`
|
|
||||||
3. Update any custom message/notification calls to use the utilities
|
|
||||||
|
|
||||||
### File Location
|
|
||||||
The centralized import file is located at: `worklenz-frontend/src/shared/antd-imports.ts`
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
### Component Creation
|
|
||||||
```typescript
|
|
||||||
import React from 'react';
|
|
||||||
import { Button, Input, Modal, EditOutlined, appMessage } from '@antd-imports';
|
|
||||||
|
|
||||||
const MyComponent = () => {
|
|
||||||
const handleClick = () => {
|
|
||||||
appMessage.success('Operation completed!');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button icon={<EditOutlined />} onClick={handleClick}>
|
|
||||||
Edit Item
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### Form Implementation
|
|
||||||
```typescript
|
|
||||||
import { Form, Input, Select, Button, DatePicker } from '@antd-imports';
|
|
||||||
|
|
||||||
const MyForm = () => {
|
|
||||||
return (
|
|
||||||
<Form layout="vertical">
|
|
||||||
<Form.Item label="Name" name="name">
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="Type" name="type">
|
|
||||||
<Select options={options} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="Date" name="date">
|
|
||||||
<DatePicker />
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
## Enforcement
|
|
||||||
|
|
||||||
This rule is **MANDATORY** and applies to:
|
|
||||||
- All new component development
|
|
||||||
- All code refactoring
|
|
||||||
- All bug fixes
|
|
||||||
- All feature implementations
|
|
||||||
|
|
||||||
**Violations will result in code review rejection.**
|
|
||||||
|
|
||||||
### File Path:
|
|
||||||
The centralized file is located at: `worklenz-frontend/src/shared/antd-imports.ts`
|
|
||||||
# Ant Design Import Rules for Worklenz
|
|
||||||
|
|
||||||
## 🚨 CRITICAL: Always Use Centralized Imports
|
|
||||||
|
|
||||||
**NEVER import Ant Design components directly from 'antd' or '@ant-design/icons'**
|
|
||||||
|
|
||||||
### ✅ Correct Import Pattern
|
|
||||||
```typescript
|
|
||||||
import { Button, Input, Select, EditOutlined, PlusOutlined } from '@antd-imports';
|
|
||||||
// or
|
|
||||||
import { Button, Input, Select, EditOutlined, PlusOutlined } from '@/shared/antd-imports';
|
|
||||||
```
|
|
||||||
|
|
||||||
### ❌ Forbidden Import Patterns
|
|
||||||
```typescript
|
|
||||||
// NEVER do this:
|
|
||||||
import { Button, Input, Select } from 'antd';
|
|
||||||
import { EditOutlined, PlusOutlined } from '@ant-design/icons';
|
|
||||||
```
|
|
||||||
|
|
||||||
## Why This Rule Exists
|
|
||||||
|
|
||||||
### Benefits of Centralized Imports:
|
|
||||||
- **Better Tree-Shaking**: Optimized bundle size through centralized management
|
|
||||||
- **Consistent React Context**: Proper context sharing across components
|
|
||||||
- **Type Safety**: Centralized TypeScript definitions
|
|
||||||
- **Maintainability**: Single source of truth for all Ant Design imports
|
|
||||||
- **Performance**: Reduced bundle size and improved loading times
|
|
||||||
|
|
||||||
## What's Available in `@antd-imports`
|
|
||||||
|
|
||||||
### Core Components
|
|
||||||
- **Layout**: Layout, Row, Col, Flex, Divider, Space
|
|
||||||
- **Navigation**: Menu, Tabs, Breadcrumb, Pagination
|
|
||||||
- **Data Entry**: Input, Select, DatePicker, TimePicker, Form, Checkbox, InputNumber
|
|
||||||
- **Data Display**: Table, List, Card, Tag, Avatar, Badge, Progress, Statistic
|
|
||||||
- **Feedback**: Modal, Drawer, Alert, Message, Notification, Spin, Skeleton, Result
|
|
||||||
- **Other**: Button, Typography, Tooltip, Popconfirm, Dropdown, ConfigProvider
|
|
||||||
|
|
||||||
### Icons
|
|
||||||
Common icons including: EditOutlined, DeleteOutlined, PlusOutlined, MoreOutlined, CheckOutlined, CloseOutlined, CalendarOutlined, UserOutlined, TeamOutlined, and many more.
|
|
||||||
|
|
||||||
### Utilities
|
|
||||||
- **appMessage**: Centralized message utility
|
|
||||||
- **appNotification**: Centralized notification utility
|
|
||||||
- **antdConfig**: Default Ant Design configuration
|
|
||||||
- **taskManagementAntdConfig**: Task-specific configuration
|
|
||||||
|
|
||||||
## Implementation Guidelines
|
|
||||||
|
|
||||||
### When Creating New Components:
|
|
||||||
1. **Always** import from `@antd-imports` or `@/shared/antd-imports`
|
|
||||||
2. Use `appMessage` and `appNotification` for user feedback
|
|
||||||
3. Apply `antdConfig` for consistent styling
|
|
||||||
4. Use `taskManagementAntdConfig` for task-related components
|
|
||||||
|
|
||||||
### When Refactoring Existing Code:
|
|
||||||
1. Replace direct 'antd' imports with `@antd-imports`
|
|
||||||
2. Replace direct '@ant-design/icons' imports with `@antd-imports`
|
|
||||||
3. Update any custom message/notification calls to use the utilities
|
|
||||||
|
|
||||||
### File Location
|
|
||||||
The centralized import file is located at: `worklenz-frontend/src/shared/antd-imports.ts`
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
### Component Creation
|
|
||||||
```typescript
|
|
||||||
import React from 'react';
|
|
||||||
import { Button, Input, Modal, EditOutlined, appMessage } from '@antd-imports';
|
|
||||||
|
|
||||||
const MyComponent = () => {
|
|
||||||
const handleClick = () => {
|
|
||||||
appMessage.success('Operation completed!');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button icon={<EditOutlined />} onClick={handleClick}>
|
|
||||||
Edit Item
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### Form Implementation
|
|
||||||
```typescript
|
|
||||||
import { Form, Input, Select, Button, DatePicker } from '@antd-imports';
|
|
||||||
|
|
||||||
const MyForm = () => {
|
|
||||||
return (
|
|
||||||
<Form layout="vertical">
|
|
||||||
<Form.Item label="Name" name="name">
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="Type" name="type">
|
|
||||||
<Select options={options} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="Date" name="date">
|
|
||||||
<DatePicker />
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
## Enforcement
|
|
||||||
|
|
||||||
This rule is **MANDATORY** and applies to:
|
|
||||||
- All new component development
|
|
||||||
- All code refactoring
|
|
||||||
- All bug fixes
|
|
||||||
- All feature implementations
|
|
||||||
|
|
||||||
**Violations will result in code review rejection.**
|
|
||||||
|
|
||||||
### File Path:
|
|
||||||
The centralized file is located at: `worklenz-frontend/src/shared/antd-imports.ts`
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
# 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.
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -36,6 +36,8 @@ lerna-debug.log*
|
|||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
.idea/
|
.idea/
|
||||||
|
.cursor/
|
||||||
|
.claude/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.suo
|
*.suo
|
||||||
*.ntvs*
|
*.ntvs*
|
||||||
|
|||||||
@@ -265,8 +265,8 @@ export default class ReportingMembersController extends ReportingControllerBase
|
|||||||
(SELECT color_code FROM project_phases WHERE id = (SELECT phase_id FROM task_phase WHERE task_id = t.id)) AS phase_color,
|
(SELECT color_code FROM project_phases WHERE id = (SELECT phase_id FROM task_phase WHERE task_id = t.id)) AS phase_color,
|
||||||
|
|
||||||
(total_minutes * 60) AS total_minutes,
|
(total_minutes * 60) AS total_minutes,
|
||||||
(SELECT SUM(time_spent) FROM task_work_log WHERE task_id = t.id AND ta.team_member_id = $1) AS time_logged,
|
(SELECT SUM(time_spent) FROM task_work_log twl WHERE twl.task_id = t.id AND twl.user_id = (SELECT user_id FROM team_members WHERE id = $1)) AS time_logged,
|
||||||
((SELECT SUM(time_spent) FROM task_work_log WHERE task_id = t.id AND ta.team_member_id = $1) - (total_minutes * 60)) AS overlogged_time`;
|
((SELECT SUM(time_spent) FROM task_work_log twl WHERE twl.task_id = t.id AND twl.user_id = (SELECT user_id FROM team_members WHERE id = $1)) - (total_minutes * 60)) AS overlogged_time`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static getActivityLogsOverdue(key: string, dateRange: string[]) {
|
protected static getActivityLogsOverdue(key: string, dateRange: string[]) {
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export async function on_quick_assign_or_remove(_io: Server, socket: Socket, dat
|
|||||||
assign_type: type
|
assign_type: type
|
||||||
});
|
});
|
||||||
|
|
||||||
if (userId !== assignment.user_id) {
|
if (assignment && userId !== assignment.user_id) {
|
||||||
NotificationsService.createTaskUpdate(
|
NotificationsService.createTaskUpdate(
|
||||||
type,
|
type,
|
||||||
userId as string,
|
userId as string,
|
||||||
@@ -109,6 +109,11 @@ export async function assignMemberIfNot(taskId: string, userId: string, teamId:
|
|||||||
const result = await db.query(q, [taskId, userId, teamId]);
|
const result = await db.query(q, [taskId, userId, teamId]);
|
||||||
const [data] = result.rows;
|
const [data] = result.rows;
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
log_error(new Error(`No team member found for userId: ${userId}, teamId: ${teamId}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
team_member_id: data.team_member_id,
|
team_member_id: data.team_member_id,
|
||||||
project_id: data.project_id,
|
project_id: data.project_id,
|
||||||
|
|||||||
@@ -38,16 +38,10 @@
|
|||||||
"updateMemberErrorMessage": "Aktualisierung des Teammitglieds fehlgeschlagen. Bitte versuchen Sie es erneut.",
|
"updateMemberErrorMessage": "Aktualisierung des Teammitglieds fehlgeschlagen. Bitte versuchen Sie es erneut.",
|
||||||
"memberText": "Mitglied",
|
"memberText": "Mitglied",
|
||||||
"adminText": "Administrator",
|
"adminText": "Administrator",
|
||||||
"guestText": "Gast (Nur Lesen)",
|
|
||||||
"ownerText": "Team-Besitzer",
|
"ownerText": "Team-Besitzer",
|
||||||
"addedText": "Hinzugefügt",
|
"addedText": "Hinzugefügt",
|
||||||
"updatedText": "Aktualisiert",
|
"updatedText": "Aktualisiert",
|
||||||
"noResultFound": "Geben Sie eine E-Mail-Adresse ein und drücken Sie Enter...",
|
"noResultFound": "Geben Sie eine E-Mail-Adresse ein und drücken Sie Enter...",
|
||||||
"jobTitlesFetchError": "Fehler beim Abrufen der Jobtitel",
|
"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"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,16 +38,10 @@
|
|||||||
"updateMemberErrorMessage": "Failed to update team member. Please try again.",
|
"updateMemberErrorMessage": "Failed to update team member. Please try again.",
|
||||||
"memberText": "Member",
|
"memberText": "Member",
|
||||||
"adminText": "Admin",
|
"adminText": "Admin",
|
||||||
"guestText": "Guest (Read-only)",
|
|
||||||
"ownerText": "Team Owner",
|
"ownerText": "Team Owner",
|
||||||
"addedText": "Added",
|
"addedText": "Added",
|
||||||
"updatedText": "Updated",
|
"updatedText": "Updated",
|
||||||
"noResultFound": "Type an email address and hit enter...",
|
"noResultFound": "Type an email address and hit enter...",
|
||||||
"jobTitlesFetchError": "Failed to fetch job titles",
|
"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"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,16 +38,10 @@
|
|||||||
"updateMemberErrorMessage": "Error al actualizar miembro del equipo. Por favor, intente nuevamente.",
|
"updateMemberErrorMessage": "Error al actualizar miembro del equipo. Por favor, intente nuevamente.",
|
||||||
"memberText": "Miembro del equipo",
|
"memberText": "Miembro del equipo",
|
||||||
"adminText": "Administrador",
|
"adminText": "Administrador",
|
||||||
"guestText": "Invitado (Solo lectura)",
|
|
||||||
"ownerText": "Propietario del equipo",
|
"ownerText": "Propietario del equipo",
|
||||||
"addedText": "Agregado",
|
"addedText": "Agregado",
|
||||||
"updatedText": "Actualizado",
|
"updatedText": "Actualizado",
|
||||||
"noResultFound": "Escriba una dirección de correo electrónico y presione enter...",
|
"noResultFound": "Escriba una dirección de correo electrónico y presione enter...",
|
||||||
"jobTitlesFetchError": "Error al obtener los cargos",
|
"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"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import Tooltip from 'antd/es/tooltip';
|
||||||
|
import Avatar from 'antd/es/avatar';
|
||||||
|
|
||||||
import { AvatarNamesMap } from '../shared/constants';
|
import { AvatarNamesMap } from '../shared/constants';
|
||||||
import { Avatar, Tooltip } from '@/shared/antd-imports';
|
|
||||||
|
|
||||||
interface CustomAvatarProps {
|
interface CustomAvatarProps {
|
||||||
avatarName: string;
|
avatarName: string;
|
||||||
|
|||||||
50
worklenz-frontend/src/components/TawkTo.tsx
Normal file
50
worklenz-frontend/src/components/TawkTo.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
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;
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
|
import { adminCenterApiService } from '@/api/admin-center/admin-center.api.service';
|
||||||
import { fetchStorageInfo } from '@/features/admin-center/admin-center.slice';
|
import { fetchStorageInfo } from '@/features/admin-center/admin-center.slice';
|
||||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||||
import { SUBSCRIPTION_STATUS } from '@/shared/constants';
|
import { SUBSCRIPTION_STATUS } from '@/shared/constants';
|
||||||
import { Card, Progress, Typography } from '@/shared/antd-imports';
|
import { IBillingAccountStorage } from '@/types/admin-center/admin-center.types';
|
||||||
|
import logger from '@/utils/errorLogger';
|
||||||
|
import { Card, Progress, Typography } from 'antd/es';
|
||||||
import { useEffect, useState, useMemo } from 'react';
|
import { useEffect, useState, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { adminCenterApiService } from '@/api/admin-center/admin-center.api.service';
|
import { adminCenterApiService } from '@/api/admin-center/admin-center.api.service';
|
||||||
import logger from '@/utils/errorLogger';
|
import logger from '@/utils/errorLogger';
|
||||||
import { EnterOutlined, EditOutlined } from '@/shared/antd-imports';
|
import { EnterOutlined, EditOutlined } from '@/shared/antd-imports';
|
||||||
import { Card, Button, Tooltip, Typography, TextArea } from '@/shared/antd-imports';
|
import { Card, Button, Tooltip, Typography } from '@/shared/antd-imports';
|
||||||
|
import TextArea from 'antd/es/input/TextArea';
|
||||||
import { TFunction } from 'i18next';
|
import { TFunction } from 'i18next';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +1,31 @@
|
|||||||
|
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 { 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 { useAppSelector } from '@/hooks/useAppSelector';
|
||||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||||
import { toggleProjectMemberDrawer } from '@features/projects/singleProject/members/projectMembersSlice';
|
import { toggleProjectMemberDrawer } from '../../../features/projects/singleProject/members/projectMembersSlice';
|
||||||
import { colors } from '@/styles/colors';
|
import { colors } from '../../../styles/colors';
|
||||||
|
import { PlusOutlined, UsergroupAddOutlined } from '@/shared/antd-imports';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import SingleAvatar from '@/components/common/single-avatar/single-avatar';
|
import SingleAvatar from '@/components/common/single-avatar/single-avatar';
|
||||||
|
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||||
import { ITeamMembersViewModel } from '@/types/teamMembers/teamMembersViewModel.types';
|
import { ITeamMembersViewModel } from '@/types/teamMembers/teamMembersViewModel.types';
|
||||||
import { sortTeamMembers } from '@/utils/sort-team-members';
|
import { sortByBooleanField, sortBySelection, sortTeamMembers } from '@/utils/sort-team-members';
|
||||||
import { useAuthService } from '@/hooks/useAuth';
|
import { useAuthService } from '@/hooks/useAuth';
|
||||||
import { useSocket } from '@/socket/socketContext';
|
import { useSocket } from '@/socket/socketContext';
|
||||||
import { SocketEvents } from '@/shared/socket-events';
|
import { SocketEvents } from '@/shared/socket-events';
|
||||||
|
import { getTeamMembers } from '@/features/team-members/team-members.slice';
|
||||||
|
|
||||||
interface BoardAssigneeSelectorProps {
|
interface BoardAssigneeSelectorProps {
|
||||||
task: IProjectTask;
|
task: IProjectTask;
|
||||||
|
|||||||
@@ -1,280 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default } from './InviteTeamMembersModal';
|
|
||||||
@@ -1,32 +1,26 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Card, Empty } from '@/shared/antd-imports';
|
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
import { RootState } from '@/app/store';
|
import { RootState } from '@/app/store';
|
||||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
|
||||||
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 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 '../EnhancedKanbanBoard.css';
|
||||||
import '../EnhancedKanbanGroup.css';
|
import '../EnhancedKanbanGroup.css';
|
||||||
import '../EnhancedKanbanTaskCard.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 { statusApiService } from '@/api/taskAttributes/status/status.api.service';
|
||||||
|
import alertService from '@/services/alerts/alertService';
|
||||||
|
import logger from '@/utils/errorLogger';
|
||||||
|
import { checkTaskDependencyStatus } from '@/utils/check-task-dependency-status';
|
||||||
|
import { useTaskSocketHandlers } from '@/hooks/useTaskSocketHandlers';
|
||||||
|
|
||||||
const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ projectId }) => {
|
const EnhancedKanbanBoardNativeDnD: React.FC<{ projectId: string }> = ({ projectId }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|||||||
@@ -1,38 +1,37 @@
|
|||||||
import React, { useCallback, useMemo, useState } from 'react';
|
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 { useSortable, defaultAnimateLayoutChanges } from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
||||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
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 { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||||
import { setShowTaskDrawer, setSelectedTaskId } from '@/features/task-drawer/task-drawer.slice';
|
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 {
|
import {
|
||||||
fetchBoardSubTasks,
|
fetchBoardSubTasks,
|
||||||
toggleTaskExpansion,
|
toggleTaskExpansion,
|
||||||
} from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
} from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
||||||
import { IProjectTask } from '@/types/project/projectTasksViewModel.types';
|
import { Divider } from '@/shared/antd-imports';
|
||||||
import { themeWiseColor } from '@/utils/themeWiseColor';
|
import { List } from '@/shared/antd-imports';
|
||||||
import './EnhancedKanbanTaskCard.css';
|
import { Skeleton } from '@/shared/antd-imports';
|
||||||
import LazyAssigneeSelectorWrapper from '../task-management/lazy-assignee-selector';
|
import { PlusOutlined } from '@/shared/antd-imports';
|
||||||
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 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 EnhancedKanbanCreateSubtaskCard from './EnhancedKanbanCreateSubtaskCard';
|
||||||
|
import LazyAssigneeSelectorWrapper from '@/components/task-management/lazy-assignee-selector';
|
||||||
import AvatarGroup from '@/components/AvatarGroup';
|
import AvatarGroup from '@/components/AvatarGroup';
|
||||||
|
|
||||||
interface EnhancedKanbanTaskCardProps {
|
interface EnhancedKanbanTaskCardProps {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { SettingOutlined, Tooltip, Button } from '@/shared/antd-imports';
|
import { SettingOutlined } from '@/shared/antd-imports';
|
||||||
|
import Tooltip from 'antd/es/tooltip';
|
||||||
|
import Button from 'antd/es/button';
|
||||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||||
import { toggleDrawer } from '../../../features/projects/status/StatusSlice';
|
import { toggleDrawer } from '../../../features/projects/status/StatusSlice';
|
||||||
import { colors } from '@/styles/colors';
|
import { colors } from '@/styles/colors';
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import { Form, Input, Select, Button, Drawer, Flex, Badge } from '@/shared/antd-imports';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
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 { useAppSelector } from '@/hooks/useAppSelector';
|
||||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||||
import { toggleDrawer } from '@/features/projects/status/StatusSlice';
|
import { toggleDrawer } from '@/features/projects/status/StatusSlice';
|
||||||
|
|
||||||
import {
|
import './create-status-drawer.css';
|
||||||
createStatus,
|
|
||||||
fetchStatusesCategories,
|
import { createStatus, fetchStatusesCategories, fetchStatuses } from '@/features/taskAttributes/taskStatusSlice';
|
||||||
fetchStatuses,
|
|
||||||
} from '@/features/taskAttributes/taskStatusSlice';
|
|
||||||
import { ITaskStatusCategory } from '@/types/status.types';
|
import { ITaskStatusCategory } from '@/types/status.types';
|
||||||
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
|
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
|
||||||
import useTabSearchParam from '@/hooks/useTabSearchParam';
|
import useTabSearchParam from '@/hooks/useTabSearchParam';
|
||||||
@@ -18,8 +22,6 @@ import { evt_project_board_create_status } from '@/shared/worklenz-analytics-eve
|
|||||||
import { fetchTaskGroups } from '@/features/tasks/tasks.slice';
|
import { fetchTaskGroups } from '@/features/tasks/tasks.slice';
|
||||||
import { fetchBoardTaskGroups } from '@/features/board/board-slice';
|
import { fetchBoardTaskGroups } from '@/features/board/board-slice';
|
||||||
|
|
||||||
import './create-status-drawer.css';
|
|
||||||
|
|
||||||
const StatusDrawer: React.FC = () => {
|
const StatusDrawer: React.FC = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { trackMixpanelEvent } = useMixpanelTracking();
|
const { trackMixpanelEvent } = useMixpanelTracking();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useCallback, useEffect, useState } from 'react';
|
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 { useTranslation } from 'react-i18next';
|
||||||
|
import Form from 'antd/es/form';
|
||||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||||
import { fetchStatuses, fetchStatusesCategories } from '@/features/taskAttributes/taskStatusSlice';
|
import { fetchStatuses, fetchStatusesCategories } from '@/features/taskAttributes/taskStatusSlice';
|
||||||
@@ -9,7 +9,9 @@ import useTabSearchParam from '@/hooks/useTabSearchParam';
|
|||||||
import { fetchTaskGroups } from '@/features/tasks/tasks.slice';
|
import { fetchTaskGroups } from '@/features/tasks/tasks.slice';
|
||||||
|
|
||||||
import { deleteStatusToggleDrawer } from '@/features/projects/status/DeleteStatusSlice';
|
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 { DownOutlined } from '@/shared/antd-imports';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
import {
|
import {
|
||||||
deleteSection,
|
deleteSection,
|
||||||
IGroupBy,
|
IGroupBy,
|
||||||
@@ -19,6 +21,7 @@ import { phasesApiService } from '@/api/taskAttributes/phases/phases.api.service
|
|||||||
import logger from '@/utils/errorLogger';
|
import logger from '@/utils/errorLogger';
|
||||||
import { fetchEnhancedKanbanGroups } from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
import { fetchEnhancedKanbanGroups } from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
|
const { Option } = Select;
|
||||||
|
|
||||||
const DeleteStatusDrawer: React.FC = () => {
|
const DeleteStatusDrawer: React.FC = () => {
|
||||||
const [currentStatus, setCurrentStatus] = useState<string>('');
|
const [currentStatus, setCurrentStatus] = useState<string>('');
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { CaretDownFilled, Dropdown, Button, Flex, ConfigProvider } from '@/shared/antd-imports';
|
import { CaretDownFilled } from '@/shared/antd-imports';
|
||||||
|
import { ConfigProvider, Flex, Dropdown, Button } from 'antd/es';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
import ConfigPhaseButton from '@features/projects/singleProject/phase/ConfigPhaseButton';
|
import ConfigPhaseButton from '@features/projects/singleProject/phase/ConfigPhaseButton';
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import {
|
import { CaretDownFilled } from '@/shared/antd-imports';
|
||||||
CaretDownFilled,
|
import Badge from 'antd/es/badge';
|
||||||
Card,
|
import Button from 'antd/es/button';
|
||||||
Flex,
|
import Card from 'antd/es/card';
|
||||||
Input,
|
import Checkbox from 'antd/es/checkbox';
|
||||||
List,
|
import Dropdown from 'antd/es/dropdown';
|
||||||
Checkbox,
|
import Empty from 'antd/es/empty';
|
||||||
Dropdown,
|
import Flex from 'antd/es/flex';
|
||||||
Button,
|
import Input, { InputRef } from 'antd/es/input';
|
||||||
Empty,
|
import List from 'antd/es/list';
|
||||||
Space,
|
import Space from 'antd/es/space';
|
||||||
InputRef,
|
|
||||||
} from '@/shared/antd-imports';
|
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { useMemo, useRef, useState, useEffect } from 'react';
|
import { useMemo, useRef, useState, useEffect } from 'react';
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { Col, ConfigProvider, Flex, Menu, MenuProps, Alert } from '@/shared/antd-imports';
|
import { Col, ConfigProvider, Flex, Menu, MenuProps, Alert } from '@/shared/antd-imports';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
import InviteTeamMembersModal from '../../components/common/invite-team-members-modal';
|
import InviteTeamMembers from '../../components/common/invite-team-members/invite-team-members';
|
||||||
import InviteButton from './invite/InviteButton';
|
import InviteButton from './invite/InviteButton';
|
||||||
import MobileMenuButton from './mobileMenu/MobileMenuButton';
|
import MobileMenuButton from './mobileMenu/MobileMenuButton';
|
||||||
import NavbarLogo from './navbar-logo';
|
import NavbarLogo from './navbar-logo';
|
||||||
@@ -179,7 +179,7 @@ const Navbar = () => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{isOwnerOrAdmin && createPortal(<InviteTeamMembersModal />, document.body, 'invite-team-members-modal')}
|
{isOwnerOrAdmin && createPortal(<InviteTeamMembers />, document.body, 'invite-team-members')}
|
||||||
{createPortal(<NotificationDrawer />, document.body, 'notification-drawer')}
|
{createPortal(<NotificationDrawer />, document.body, 'notification-drawer')}
|
||||||
</Col>
|
</Col>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -67,8 +67,6 @@ import {
|
|||||||
Radio,
|
Radio,
|
||||||
} from 'antd/es';
|
} from 'antd/es';
|
||||||
|
|
||||||
import TextArea from 'antd/es/input/TextArea';
|
|
||||||
|
|
||||||
// Icons - Import commonly used ones
|
// Icons - Import commonly used ones
|
||||||
export {
|
export {
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
@@ -242,7 +240,6 @@ export {
|
|||||||
Timeline,
|
Timeline,
|
||||||
Mentions,
|
Mentions,
|
||||||
Radio,
|
Radio,
|
||||||
TextArea
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// TypeScript Types - Import commonly used ones
|
// TypeScript Types - Import commonly used ones
|
||||||
@@ -266,7 +263,6 @@ export type {
|
|||||||
PaginationProps,
|
PaginationProps,
|
||||||
CollapseProps,
|
CollapseProps,
|
||||||
TablePaginationConfig,
|
TablePaginationConfig,
|
||||||
CheckboxChangeEvent
|
|
||||||
} from 'antd/es';
|
} from 'antd/es';
|
||||||
|
|
||||||
// Dayjs
|
// Dayjs
|
||||||
|
|||||||
@@ -4,5 +4,4 @@ export interface ITeamMemberCreateRequest extends ITeamMember {
|
|||||||
job_title?: string | null;
|
job_title?: string | null;
|
||||||
emails?: string | string[];
|
emails?: string | string[];
|
||||||
is_admin?: boolean;
|
is_admin?: boolean;
|
||||||
is_guest?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user