feat(localization): update and enhance localization files for multiple languages
- Updated localization files for various languages, including English, German, Spanish, Portuguese, and Chinese, to ensure consistency and accuracy across the application. - Added new keys and updated existing ones to support recent UI changes and features, particularly in project views, task lists, and admin center settings. - Enhanced the structure of localization files to improve maintainability and facilitate future updates. - Implemented performance optimizations in the frontend components to better handle localization data.
This commit is contained in:
@@ -1,9 +1,18 @@
|
||||
import { adminCenterApiService } from '@/api/admin-center/admin-center.api.service';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
IBillingAccountInfo,
|
||||
IBillingAccountStorage,
|
||||
IFreePlanSettings,
|
||||
IOrganization,
|
||||
IOrganizationAdmin,
|
||||
} from '@/types/admin-center/admin-center.types';
|
||||
import {
|
||||
IOrganizationHolidaySettings,
|
||||
ICountryWithStates,
|
||||
IHolidayCalendarEvent,
|
||||
} from '@/types/holiday/holiday.types';
|
||||
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
interface adminCenterState {
|
||||
@@ -14,6 +23,17 @@ interface adminCenterState {
|
||||
freePlanSettings: IFreePlanSettings | null;
|
||||
storageInfo: IBillingAccountStorage | null;
|
||||
loadingStorageInfo: boolean;
|
||||
organization: IOrganization | null;
|
||||
loadingOrganization: boolean;
|
||||
organizationAdmins: IOrganizationAdmin[] | null;
|
||||
loadingOrganizationAdmins: boolean;
|
||||
holidaySettings: IOrganizationHolidaySettings | null;
|
||||
loadingHolidaySettings: boolean;
|
||||
countriesWithStates: ICountryWithStates[];
|
||||
loadingCountries: boolean;
|
||||
holidays: IHolidayCalendarEvent[];
|
||||
loadingHolidays: boolean;
|
||||
holidaysDateRange: { from: string; to: string } | null;
|
||||
}
|
||||
|
||||
const initialState: adminCenterState = {
|
||||
@@ -24,6 +44,17 @@ const initialState: adminCenterState = {
|
||||
freePlanSettings: null,
|
||||
storageInfo: null,
|
||||
loadingStorageInfo: false,
|
||||
organization: null,
|
||||
loadingOrganization: false,
|
||||
organizationAdmins: null,
|
||||
loadingOrganizationAdmins: false,
|
||||
holidaySettings: null,
|
||||
loadingHolidaySettings: false,
|
||||
countriesWithStates: [],
|
||||
loadingCountries: false,
|
||||
holidays: [],
|
||||
loadingHolidays: false,
|
||||
holidaysDateRange: null,
|
||||
};
|
||||
|
||||
export const fetchBillingInfo = createAsyncThunk('adminCenter/fetchBillingInfo', async () => {
|
||||
@@ -44,6 +75,81 @@ export const fetchStorageInfo = createAsyncThunk('adminCenter/fetchStorageInfo',
|
||||
return res.body;
|
||||
});
|
||||
|
||||
export const fetchOrganizationDetails = createAsyncThunk(
|
||||
'adminCenter/fetchOrganizationDetails',
|
||||
async () => {
|
||||
const res = await adminCenterApiService.getOrganizationDetails();
|
||||
return res.body;
|
||||
}
|
||||
);
|
||||
|
||||
export const fetchOrganizationAdmins = createAsyncThunk(
|
||||
'adminCenter/fetchOrganizationAdmins',
|
||||
async () => {
|
||||
const res = await adminCenterApiService.getOrganizationAdmins();
|
||||
return res.body;
|
||||
}
|
||||
);
|
||||
|
||||
export const fetchHolidaySettings = createAsyncThunk(
|
||||
'adminCenter/fetchHolidaySettings',
|
||||
async () => {
|
||||
const { holidayApiService } = await import('@/api/holiday/holiday.api.service');
|
||||
const res = await holidayApiService.getOrganizationHolidaySettings();
|
||||
return res.body;
|
||||
}
|
||||
);
|
||||
|
||||
export const updateHolidaySettings = createAsyncThunk(
|
||||
'adminCenter/updateHolidaySettings',
|
||||
async (settings: IOrganizationHolidaySettings) => {
|
||||
const { holidayApiService } = await import('@/api/holiday/holiday.api.service');
|
||||
await holidayApiService.updateOrganizationHolidaySettings(settings);
|
||||
await adminCenterApiService.updateOrganizationHolidaySettings(settings);
|
||||
return settings;
|
||||
}
|
||||
);
|
||||
|
||||
export const fetchCountriesWithStates = createAsyncThunk(
|
||||
'adminCenter/fetchCountriesWithStates',
|
||||
async () => {
|
||||
const { holidayApiService } = await import('@/api/holiday/holiday.api.service');
|
||||
const res = await holidayApiService.getCountriesWithStates();
|
||||
return res.body;
|
||||
}
|
||||
);
|
||||
|
||||
export const fetchHolidays = createAsyncThunk(
|
||||
'adminCenter/fetchHolidays',
|
||||
async (
|
||||
params: { from_date: string; to_date: string; include_custom?: boolean; country_code?: string },
|
||||
{ getState }
|
||||
) => {
|
||||
const state = getState() as { adminCenterReducer: adminCenterState };
|
||||
const currentRange = state.adminCenterReducer.holidaysDateRange;
|
||||
|
||||
// Get country code from holiday settings if not provided in params
|
||||
const countryCode = params.country_code || state.adminCenterReducer.holidaySettings?.country_code;
|
||||
|
||||
// Check if we already have data for this range (cache hit)
|
||||
if (
|
||||
currentRange &&
|
||||
currentRange.from === params.from_date &&
|
||||
currentRange.to === params.to_date &&
|
||||
state.adminCenterReducer.holidays.length > 0
|
||||
) {
|
||||
return { holidays: state.adminCenterReducer.holidays, dateRange: currentRange };
|
||||
}
|
||||
|
||||
const { holidayApiService } = await import('@/api/holiday/holiday.api.service');
|
||||
const res = await holidayApiService.getCombinedHolidays({
|
||||
...params,
|
||||
country_code: countryCode,
|
||||
});
|
||||
return { holidays: res.body, dateRange: { from: params.from_date, to: params.to_date } };
|
||||
}
|
||||
);
|
||||
|
||||
const adminCenterSlice = createSlice({
|
||||
name: 'adminCenterReducer',
|
||||
initialState,
|
||||
@@ -86,8 +192,123 @@ const adminCenterSlice = createSlice({
|
||||
builder.addCase(fetchStorageInfo.pending, (state, action) => {
|
||||
state.loadingStorageInfo = true;
|
||||
});
|
||||
|
||||
builder.addCase(fetchOrganizationDetails.pending, (state, action) => {
|
||||
state.loadingOrganization = true;
|
||||
});
|
||||
builder.addCase(fetchOrganizationDetails.fulfilled, (state, action) => {
|
||||
state.organization = action.payload;
|
||||
state.loadingOrganization = false;
|
||||
});
|
||||
builder.addCase(fetchOrganizationDetails.rejected, (state, action) => {
|
||||
state.loadingOrganization = false;
|
||||
});
|
||||
|
||||
builder.addCase(fetchOrganizationAdmins.pending, (state, action) => {
|
||||
state.loadingOrganizationAdmins = true;
|
||||
});
|
||||
builder.addCase(fetchOrganizationAdmins.fulfilled, (state, action) => {
|
||||
state.organizationAdmins = action.payload;
|
||||
state.loadingOrganizationAdmins = false;
|
||||
});
|
||||
builder.addCase(fetchOrganizationAdmins.rejected, (state, action) => {
|
||||
state.loadingOrganizationAdmins = false;
|
||||
});
|
||||
|
||||
builder.addCase(fetchHolidaySettings.pending, (state, action) => {
|
||||
state.loadingHolidaySettings = true;
|
||||
});
|
||||
builder.addCase(fetchHolidaySettings.fulfilled, (state, action) => {
|
||||
state.holidaySettings = action.payload;
|
||||
state.loadingHolidaySettings = false;
|
||||
});
|
||||
builder.addCase(fetchHolidaySettings.rejected, (state, action) => {
|
||||
state.loadingHolidaySettings = false;
|
||||
});
|
||||
|
||||
builder.addCase(updateHolidaySettings.fulfilled, (state, action) => {
|
||||
state.holidaySettings = action.payload;
|
||||
// Update organization object if it exists
|
||||
if (state.organization) {
|
||||
state.organization.country_code = action.payload.country_code;
|
||||
state.organization.state_code = action.payload.state_code;
|
||||
state.organization.auto_sync_holidays = action.payload.auto_sync_holidays;
|
||||
}
|
||||
});
|
||||
|
||||
builder.addCase(fetchCountriesWithStates.pending, (state, action) => {
|
||||
state.loadingCountries = true;
|
||||
});
|
||||
builder.addCase(fetchCountriesWithStates.fulfilled, (state, action) => {
|
||||
state.countriesWithStates = action.payload;
|
||||
state.loadingCountries = false;
|
||||
});
|
||||
builder.addCase(fetchCountriesWithStates.rejected, (state, action) => {
|
||||
state.loadingCountries = false;
|
||||
});
|
||||
|
||||
builder.addCase(fetchHolidays.pending, (state, action) => {
|
||||
state.loadingHolidays = true;
|
||||
});
|
||||
builder.addCase(fetchHolidays.fulfilled, (state, action) => {
|
||||
// Only update if this is new data (not a cache hit)
|
||||
if (action.payload.holidays !== state.holidays) {
|
||||
state.holidays = action.payload.holidays;
|
||||
state.holidaysDateRange = action.payload.dateRange;
|
||||
}
|
||||
state.loadingHolidays = false;
|
||||
});
|
||||
builder.addCase(fetchHolidays.rejected, (state, action) => {
|
||||
state.loadingHolidays = false;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { toggleRedeemCodeDrawer, toggleUpgradeModal } = adminCenterSlice.actions;
|
||||
|
||||
// Selectors for optimized access
|
||||
export const selectHolidaysByDateRange = createSelector(
|
||||
[
|
||||
(state: any) => state.adminCenterReducer.holidays,
|
||||
(state: any, dateRange: { from: string; to: string }) => dateRange,
|
||||
],
|
||||
(holidays, dateRange) => {
|
||||
if (!holidays || holidays.length === 0) return [];
|
||||
|
||||
return holidays.filter((holiday: IHolidayCalendarEvent) => {
|
||||
const holidayDate = dayjs(holiday.date);
|
||||
return holidayDate.isBetween(dayjs(dateRange.from), dayjs(dateRange.to), 'day', '[]');
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export const selectWorkingDaysInRange = createSelector(
|
||||
[
|
||||
(state: any) => state.adminCenterReducer.holidays,
|
||||
(state: any, params: { from: string; to: string; workingDays: string[] }) => params,
|
||||
],
|
||||
(holidays, { from, to, workingDays }) => {
|
||||
const start = dayjs(from);
|
||||
const end = dayjs(to);
|
||||
const holidayDates = new Set(holidays.map((h: IHolidayCalendarEvent) => h.date));
|
||||
|
||||
let totalWorkingDays = 0;
|
||||
let current = start;
|
||||
|
||||
while (current.isSameOrBefore(end)) {
|
||||
const dayName = current.format('dddd');
|
||||
const isWorkingDay = workingDays.includes(dayName);
|
||||
const isHoliday = holidayDates.has(current.format('YYYY-MM-DD'));
|
||||
|
||||
if (isWorkingDay && !isHoliday) {
|
||||
totalWorkingDays++;
|
||||
}
|
||||
|
||||
current = current.add(1, 'day');
|
||||
}
|
||||
|
||||
return totalWorkingDays;
|
||||
}
|
||||
);
|
||||
|
||||
export default adminCenterSlice.reducer;
|
||||
|
||||
@@ -124,24 +124,24 @@ const financeSlice = createSlice({
|
||||
name: 'financeReducer',
|
||||
initialState,
|
||||
reducers: {
|
||||
toggleRatecardDrawer: (state) => {
|
||||
toggleRatecardDrawer: state => {
|
||||
state.isRatecardDrawerOpen = !state.isRatecardDrawerOpen;
|
||||
},
|
||||
toggleFinanceDrawer: (state) => {
|
||||
toggleFinanceDrawer: state => {
|
||||
state.isFinanceDrawerOpen = !state.isFinanceDrawerOpen;
|
||||
},
|
||||
openFinanceDrawer: (state, action: PayloadAction<any>) => {
|
||||
state.isFinanceDrawerOpen = true;
|
||||
state.selectedTask = action.payload;
|
||||
},
|
||||
closeFinanceDrawer: (state) => {
|
||||
closeFinanceDrawer: state => {
|
||||
state.isFinanceDrawerOpen = false;
|
||||
state.selectedTask = null;
|
||||
},
|
||||
setSelectedTask: (state, action: PayloadAction<any>) => {
|
||||
state.selectedTask = action.payload;
|
||||
},
|
||||
toggleImportRatecardsDrawer: (state) => {
|
||||
toggleImportRatecardsDrawer: state => {
|
||||
state.isImportRatecardsDrawerOpen = !state.isImportRatecardsDrawerOpen;
|
||||
},
|
||||
changeCurrency: (state, action: PayloadAction<string>) => {
|
||||
@@ -150,13 +150,13 @@ const financeSlice = createSlice({
|
||||
ratecardDrawerLoading: (state, action: PayloadAction<boolean>) => {
|
||||
state.isFinanceDrawerloading = action.payload;
|
||||
},
|
||||
clearDrawerRatecard: (state) => {
|
||||
clearDrawerRatecard: state => {
|
||||
state.drawerRatecard = null;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
extraReducers: builder => {
|
||||
builder
|
||||
.addCase(fetchRateCards.pending, (state) => {
|
||||
.addCase(fetchRateCards.pending, state => {
|
||||
state.isRatecardsLoading = true;
|
||||
})
|
||||
.addCase(fetchRateCards.fulfilled, (state, action) => {
|
||||
@@ -164,14 +164,14 @@ const financeSlice = createSlice({
|
||||
state.ratecardsList = Array.isArray(action.payload.data)
|
||||
? action.payload.data
|
||||
: Array.isArray(action.payload)
|
||||
? action.payload
|
||||
: [];
|
||||
? action.payload
|
||||
: [];
|
||||
})
|
||||
.addCase(fetchRateCards.rejected, (state) => {
|
||||
.addCase(fetchRateCards.rejected, state => {
|
||||
state.isRatecardsLoading = false;
|
||||
state.ratecardsList = [];
|
||||
})
|
||||
.addCase(fetchRateCardById.pending, (state) => {
|
||||
.addCase(fetchRateCardById.pending, state => {
|
||||
state.isFinanceDrawerloading = true;
|
||||
state.drawerRatecard = null;
|
||||
})
|
||||
@@ -179,12 +179,12 @@ const financeSlice = createSlice({
|
||||
state.isFinanceDrawerloading = false;
|
||||
state.drawerRatecard = action.payload;
|
||||
})
|
||||
.addCase(fetchRateCardById.rejected, (state) => {
|
||||
.addCase(fetchRateCardById.rejected, state => {
|
||||
state.isFinanceDrawerloading = false;
|
||||
state.drawerRatecard = null;
|
||||
})
|
||||
// Create rate card
|
||||
.addCase(createRateCard.pending, (state) => {
|
||||
.addCase(createRateCard.pending, state => {
|
||||
state.isFinanceDrawerloading = true;
|
||||
})
|
||||
.addCase(createRateCard.fulfilled, (state, action) => {
|
||||
@@ -195,11 +195,11 @@ const financeSlice = createSlice({
|
||||
state.ratecardsList = [action.payload];
|
||||
}
|
||||
})
|
||||
.addCase(createRateCard.rejected, (state) => {
|
||||
.addCase(createRateCard.rejected, state => {
|
||||
state.isFinanceDrawerloading = false;
|
||||
})
|
||||
// Update rate card
|
||||
.addCase(updateRateCard.pending, (state) => {
|
||||
.addCase(updateRateCard.pending, state => {
|
||||
state.isFinanceDrawerloading = true;
|
||||
})
|
||||
.addCase(updateRateCard.fulfilled, (state, action) => {
|
||||
@@ -214,11 +214,11 @@ const financeSlice = createSlice({
|
||||
}
|
||||
}
|
||||
})
|
||||
.addCase(updateRateCard.rejected, (state) => {
|
||||
.addCase(updateRateCard.rejected, state => {
|
||||
state.isFinanceDrawerloading = false;
|
||||
})
|
||||
// Delete rate card
|
||||
.addCase(deleteRateCard.pending, (state) => {
|
||||
.addCase(deleteRateCard.pending, state => {
|
||||
state.isFinanceDrawerloading = true;
|
||||
})
|
||||
.addCase(deleteRateCard.fulfilled, (state, action) => {
|
||||
@@ -232,7 +232,7 @@ const financeSlice = createSlice({
|
||||
state.drawerRatecard = null;
|
||||
}
|
||||
})
|
||||
.addCase(deleteRateCard.rejected, (state) => {
|
||||
.addCase(deleteRateCard.rejected, state => {
|
||||
state.isFinanceDrawerloading = false;
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import { ClockCircleOutlined, StopOutlined } from '@/shared/antd-imports';
|
||||
import { Badge, Button, Dropdown, List, Tooltip, Typography, Space, Divider, theme } from '@/shared/antd-imports';
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Dropdown,
|
||||
List,
|
||||
Tooltip,
|
||||
Typography,
|
||||
Space,
|
||||
Divider,
|
||||
theme,
|
||||
} from '@/shared/antd-imports';
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { taskTimeLogsApiService, IRunningTimer } from '@/api/tasks/task-time-logs.api.service';
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { UserOutlined } from '@/shared/antd-imports';
|
||||
import { Button, Card, Dropdown, Flex, MenuProps, Tooltip, Typography } from '@/shared/antd-imports';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Dropdown,
|
||||
Flex,
|
||||
MenuProps,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from '@/shared/antd-imports';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -23,22 +23,22 @@ export const fetchProjectData = createAsyncThunk(
|
||||
if (!projectId) {
|
||||
throw new Error('Project ID is required');
|
||||
}
|
||||
|
||||
|
||||
console.log(`Fetching project data for ID: ${projectId}`);
|
||||
const response = await projectsApiService.getProject(projectId);
|
||||
|
||||
|
||||
if (!response) {
|
||||
throw new Error('No response received from API');
|
||||
}
|
||||
|
||||
|
||||
if (!response.done) {
|
||||
throw new Error(response.message || 'API request failed');
|
||||
}
|
||||
|
||||
|
||||
if (!response.body) {
|
||||
throw new Error('No project data in response body');
|
||||
}
|
||||
|
||||
|
||||
console.log(`Successfully fetched project data:`, response.body);
|
||||
return response.body;
|
||||
} catch (error) {
|
||||
|
||||
@@ -219,7 +219,7 @@ export const {
|
||||
setProjectView,
|
||||
updatePhaseLabel,
|
||||
setRefreshTimestamp,
|
||||
updateProjectCurrency
|
||||
updateProjectCurrency,
|
||||
} = projectSlice.actions;
|
||||
|
||||
export default projectSlice.reducer;
|
||||
|
||||
@@ -21,11 +21,7 @@ const ConfigPhaseButton = () => {
|
||||
className="borderless-icon-btn"
|
||||
style={{ backgroundColor: colors.transparent, boxShadow: 'none' }}
|
||||
onClick={() => dispatch(toggleDrawer())}
|
||||
icon={
|
||||
<SettingOutlined
|
||||
style={{ color: themeMode === 'dark' ? colors.white : 'black' }}
|
||||
/>
|
||||
}
|
||||
icon={<SettingOutlined style={{ color: themeMode === 'dark' ? colors.white : 'black' }} />}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -64,7 +64,11 @@ const taskListCustomColumnsSlice = createSlice({
|
||||
},
|
||||
setCustomColumnModalAttributes: (
|
||||
state,
|
||||
action: PayloadAction<{ modalType: 'create' | 'edit'; columnId: string | null; columnData?: any }>
|
||||
action: PayloadAction<{
|
||||
modalType: 'create' | 'edit';
|
||||
columnId: string | null;
|
||||
columnData?: any;
|
||||
}>
|
||||
) => {
|
||||
state.customColumnModalType = action.payload.modalType;
|
||||
state.customColumnId = action.payload.columnId;
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { memo } from 'react';
|
||||
import { ConfigProvider, Flex, Skeleton, Spin, Table, TableColumnsType, Typography } from '@/shared/antd-imports';
|
||||
import {
|
||||
ConfigProvider,
|
||||
Flex,
|
||||
Skeleton,
|
||||
Spin,
|
||||
Table,
|
||||
TableColumnsType,
|
||||
Typography,
|
||||
} from '@/shared/antd-imports';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CustomTableTitle from '@components/CustomTableTitle';
|
||||
import { simpleDateFormat } from '@/utils/simpleDateFormat';
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { Badge, Collapse, Flex, Table, TableColumnsType, Tag, Typography } from '@/shared/antd-imports';
|
||||
import {
|
||||
Badge,
|
||||
Collapse,
|
||||
Flex,
|
||||
Table,
|
||||
TableColumnsType,
|
||||
Tag,
|
||||
Typography,
|
||||
} from '@/shared/antd-imports';
|
||||
import CustomTableTitle from '@components/CustomTableTitle';
|
||||
import { colors } from '@/styles/colors';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { Badge, Collapse, Flex, Table, TableColumnsType, Tag, Typography } from '@/shared/antd-imports';
|
||||
import {
|
||||
Badge,
|
||||
Collapse,
|
||||
Flex,
|
||||
Table,
|
||||
TableColumnsType,
|
||||
Tag,
|
||||
Typography,
|
||||
} from '@/shared/antd-imports';
|
||||
import { useEffect } from 'react';
|
||||
import CustomTableTitle from '@/components/CustomTableTitle';
|
||||
import { colors } from '@/styles/colors';
|
||||
|
||||
@@ -85,7 +85,10 @@ const groupingSlice = createSlice({
|
||||
state.groupOrder.phase = action.payload;
|
||||
},
|
||||
|
||||
updateGroupOrder: (state, action: PayloadAction<{ groupType: keyof LocalGroupingState['groupOrder']; order: string[] }>) => {
|
||||
updateGroupOrder: (
|
||||
state,
|
||||
action: PayloadAction<{ groupType: keyof LocalGroupingState['groupOrder']; order: string[] }>
|
||||
) => {
|
||||
const { groupType, order } = action.payload;
|
||||
state.groupOrder[groupType] = order;
|
||||
},
|
||||
@@ -143,7 +146,7 @@ export const selectCollapsedGroupsArray = (state: RootState) => state.grouping.c
|
||||
// Memoized selector to prevent unnecessary re-renders
|
||||
export const selectCollapsedGroups = createSelector(
|
||||
[selectCollapsedGroupsArray],
|
||||
(collapsedGroupsArray) => new Set(collapsedGroupsArray)
|
||||
collapsedGroupsArray => new Set(collapsedGroupsArray)
|
||||
);
|
||||
|
||||
export const selectIsGroupCollapsed = (state: RootState, groupId: string) =>
|
||||
@@ -169,25 +172,27 @@ export const selectTaskGroups = createSelector(
|
||||
const groupValues =
|
||||
groupOrder.length > 0
|
||||
? groupOrder
|
||||
: Array.from(new Set(
|
||||
tasks.map(task => {
|
||||
if (currentGrouping === 'status') return task.status;
|
||||
if (currentGrouping === 'priority') return task.priority;
|
||||
if (currentGrouping === 'phase') {
|
||||
// For phase grouping, use 'Unmapped' for tasks without a phase
|
||||
if (!task.phase || task.phase.trim() === '') {
|
||||
return 'Unmapped';
|
||||
} else {
|
||||
return task.phase;
|
||||
: Array.from(
|
||||
new Set(
|
||||
tasks.map(task => {
|
||||
if (currentGrouping === 'status') return task.status;
|
||||
if (currentGrouping === 'priority') return task.priority;
|
||||
if (currentGrouping === 'phase') {
|
||||
// For phase grouping, use 'Unmapped' for tasks without a phase
|
||||
if (!task.phase || task.phase.trim() === '') {
|
||||
return 'Unmapped';
|
||||
} else {
|
||||
return task.phase;
|
||||
}
|
||||
}
|
||||
}
|
||||
return task.phase;
|
||||
})
|
||||
));
|
||||
return task.phase;
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
groupValues.forEach(value => {
|
||||
if (!value) return; // Skip undefined values
|
||||
|
||||
|
||||
const tasksInGroup = tasks
|
||||
.filter(task => {
|
||||
if (currentGrouping === 'status') return task.status === value;
|
||||
|
||||
@@ -63,8 +63,10 @@ export const {
|
||||
} = selectionSlice.actions;
|
||||
|
||||
// Selectors
|
||||
export const selectSelectedTaskIds = (state: RootState) => state.taskManagementSelection.selectedTaskIds;
|
||||
export const selectLastSelectedTaskId = (state: RootState) => state.taskManagementSelection.lastSelectedTaskId;
|
||||
export const selectSelectedTaskIds = (state: RootState) =>
|
||||
state.taskManagementSelection.selectedTaskIds;
|
||||
export const selectLastSelectedTaskId = (state: RootState) =>
|
||||
state.taskManagementSelection.lastSelectedTaskId;
|
||||
export const selectIsTaskSelected = (state: RootState, taskId: string) =>
|
||||
state.taskManagementSelection.selectedTaskIds.includes(taskId);
|
||||
|
||||
|
||||
@@ -7,7 +7,13 @@ import {
|
||||
EntityId,
|
||||
createSelector,
|
||||
} from '@reduxjs/toolkit';
|
||||
import { Task, TaskManagementState, TaskGroup, TaskGrouping, getSortOrderField } from '@/types/task-management.types';
|
||||
import {
|
||||
Task,
|
||||
TaskManagementState,
|
||||
TaskGroup,
|
||||
TaskGrouping,
|
||||
getSortOrderField,
|
||||
} from '@/types/task-management.types';
|
||||
import { ITaskListColumn } from '@/types/tasks/taskList.types';
|
||||
import { RootState } from '@/app/store';
|
||||
import {
|
||||
@@ -148,7 +154,7 @@ export const fetchTasks = createAsyncThunk(
|
||||
group.tasks.map((task: any) => ({
|
||||
id: task.id,
|
||||
task_key: task.task_key || '',
|
||||
title: (task.title && task.title.trim()) ? task.title.trim() : DEFAULT_TASK_NAME,
|
||||
title: task.title && task.title.trim() ? task.title.trim() : DEFAULT_TASK_NAME,
|
||||
description: task.description || '',
|
||||
status: statusIdToNameMap[task.status] || 'todo',
|
||||
priority: priorityIdToNameMap[task.priority] || 'medium',
|
||||
@@ -254,11 +260,11 @@ export const fetchTasksV3 = createAsyncThunk(
|
||||
// Ensure tasks are properly normalized
|
||||
const tasks: Task[] = response.body.allTasks.map((task: any) => {
|
||||
const now = new Date().toISOString();
|
||||
|
||||
|
||||
const transformedTask = {
|
||||
id: task.id,
|
||||
task_key: task.task_key || task.key || '',
|
||||
title: (task.title && task.title.trim()) ? task.title.trim() : DEFAULT_TASK_NAME,
|
||||
title: task.title && task.title.trim() ? task.title.trim() : DEFAULT_TASK_NAME,
|
||||
description: task.description || '',
|
||||
status: task.status || 'todo',
|
||||
priority: task.priority || 'medium',
|
||||
@@ -266,18 +272,31 @@ export const fetchTasksV3 = createAsyncThunk(
|
||||
progress: typeof task.complete_ratio === 'number' ? task.complete_ratio : 0,
|
||||
assignees: task.assignees?.map((a: { team_member_id: string }) => a.team_member_id) || [],
|
||||
assignee_names: task.assignee_names || task.names || [],
|
||||
labels: task.labels?.map((l: { id: string; label_id: string; name: string; color: string; end: boolean; names: string[] }) => ({
|
||||
id: l.id || l.label_id,
|
||||
name: l.name,
|
||||
color: l.color || '#1890ff',
|
||||
end: l.end,
|
||||
names: l.names,
|
||||
})) || [],
|
||||
all_labels: task.all_labels?.map((l: { id: string; label_id: string; name: string; color_code: string }) => ({
|
||||
id: l.id || l.label_id,
|
||||
name: l.name,
|
||||
color_code: l.color_code || '#1890ff',
|
||||
})) || [],
|
||||
labels:
|
||||
task.labels?.map(
|
||||
(l: {
|
||||
id: string;
|
||||
label_id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
end: boolean;
|
||||
names: string[];
|
||||
}) => ({
|
||||
id: l.id || l.label_id,
|
||||
name: l.name,
|
||||
color: l.color || '#1890ff',
|
||||
end: l.end,
|
||||
names: l.names,
|
||||
})
|
||||
) || [],
|
||||
all_labels:
|
||||
task.all_labels?.map(
|
||||
(l: { id: string; label_id: string; name: string; color_code: string }) => ({
|
||||
id: l.id || l.label_id,
|
||||
name: l.name,
|
||||
color_code: l.color_code || '#1890ff',
|
||||
})
|
||||
) || [],
|
||||
dueDate: task.dueDate,
|
||||
startDate: task.startDate,
|
||||
timeTracking: {
|
||||
@@ -305,7 +324,7 @@ export const fetchTasksV3 = createAsyncThunk(
|
||||
schedule_id: task.schedule_id || null,
|
||||
reporter: task.reporter || undefined,
|
||||
};
|
||||
|
||||
|
||||
return transformedTask;
|
||||
});
|
||||
|
||||
@@ -514,10 +533,13 @@ const taskManagementSlice = createSlice({
|
||||
setTasks: (state, action: PayloadAction<Task[]>) => {
|
||||
const tasks = action.payload;
|
||||
state.ids = tasks.map(task => task.id);
|
||||
state.entities = tasks.reduce((acc, task) => {
|
||||
acc[task.id] = task;
|
||||
return acc;
|
||||
}, {} as Record<string, Task>);
|
||||
state.entities = tasks.reduce(
|
||||
(acc, task) => {
|
||||
acc[task.id] = task;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, Task>
|
||||
);
|
||||
},
|
||||
addTask: (state, action: PayloadAction<Task>) => {
|
||||
const task = action.payload;
|
||||
@@ -526,11 +548,11 @@ const taskManagementSlice = createSlice({
|
||||
},
|
||||
addTaskToGroup: (state, action: PayloadAction<{ task: Task; groupId: string }>) => {
|
||||
const { task, groupId } = action.payload;
|
||||
|
||||
|
||||
state.ids.push(task.id);
|
||||
state.entities[task.id] = task;
|
||||
let group = state.groups.find(g => g.id === groupId);
|
||||
|
||||
|
||||
// If group doesn't exist and it's "Unmapped", create it dynamically
|
||||
if (!group && groupId === 'Unmapped') {
|
||||
const unmappedGroup = {
|
||||
@@ -539,12 +561,12 @@ const taskManagementSlice = createSlice({
|
||||
taskIds: [],
|
||||
type: 'phase' as const,
|
||||
color: '#fbc84c69',
|
||||
groupValue: 'Unmapped'
|
||||
groupValue: 'Unmapped',
|
||||
};
|
||||
state.groups.push(unmappedGroup);
|
||||
group = unmappedGroup;
|
||||
}
|
||||
|
||||
|
||||
if (group) {
|
||||
group.taskIds.push(task.id);
|
||||
}
|
||||
@@ -554,14 +576,18 @@ const taskManagementSlice = createSlice({
|
||||
// Additionally, update the task within its group if necessary (e.g., if status changed)
|
||||
const updatedTask = action.payload;
|
||||
const oldTask = state.entities[updatedTask.id];
|
||||
|
||||
if (oldTask && state.grouping?.id === IGroupBy.STATUS && oldTask.status !== updatedTask.status) {
|
||||
|
||||
if (
|
||||
oldTask &&
|
||||
state.grouping?.id === IGroupBy.STATUS &&
|
||||
oldTask.status !== updatedTask.status
|
||||
) {
|
||||
// Remove from old status group
|
||||
const oldGroup = state.groups.find(group => group.id === oldTask.status);
|
||||
if (oldGroup) {
|
||||
oldGroup.taskIds = oldGroup.taskIds.filter(id => id !== updatedTask.id);
|
||||
}
|
||||
|
||||
|
||||
// Add to new status group
|
||||
const newGroup = state.groups.find(group => group.id === updatedTask.status);
|
||||
if (newGroup) {
|
||||
@@ -626,8 +652,8 @@ const taskManagementSlice = createSlice({
|
||||
group.id === targetGroupId
|
||||
? [...group.taskIds, taskId]
|
||||
: group.id === sourceGroupId
|
||||
? group.taskIds.filter(id => id !== taskId)
|
||||
: group.taskIds,
|
||||
? group.taskIds.filter(id => id !== taskId)
|
||||
: group.taskIds,
|
||||
}));
|
||||
},
|
||||
optimisticTaskMove: (
|
||||
@@ -645,8 +671,8 @@ const taskManagementSlice = createSlice({
|
||||
group.id === targetGroupId
|
||||
? [...group.taskIds, taskId]
|
||||
: group.id === sourceGroupId
|
||||
? group.taskIds.filter(id => id !== taskId)
|
||||
: group.taskIds,
|
||||
? group.taskIds.filter(id => id !== taskId)
|
||||
: group.taskIds,
|
||||
}));
|
||||
},
|
||||
reorderTasksInGroup: (
|
||||
@@ -659,15 +685,15 @@ const taskManagementSlice = createSlice({
|
||||
}>
|
||||
) => {
|
||||
const { sourceTaskId, destinationTaskId, sourceGroupId, destinationGroupId } = action.payload;
|
||||
|
||||
|
||||
// Get a mutable copy of entities for updates
|
||||
const newEntities = { ...state.entities };
|
||||
|
||||
|
||||
const sourceTask = newEntities[sourceTaskId];
|
||||
const destinationTask = newEntities[destinationTaskId];
|
||||
|
||||
|
||||
if (!sourceTask || !destinationTask) return;
|
||||
|
||||
|
||||
if (sourceGroupId === destinationGroupId) {
|
||||
// Reordering within the same group
|
||||
const group = state.groups.find(g => g.id === sourceGroupId);
|
||||
@@ -715,7 +741,7 @@ const taskManagementSlice = createSlice({
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Update the state's entities after all modifications
|
||||
state.entities = newEntities;
|
||||
},
|
||||
@@ -734,7 +760,7 @@ const taskManagementSlice = createSlice({
|
||||
setArchived: (state, action: PayloadAction<boolean>) => {
|
||||
state.archived = action.payload;
|
||||
},
|
||||
toggleArchived: (state) => {
|
||||
toggleArchived: state => {
|
||||
state.archived = !state.archived;
|
||||
},
|
||||
resetTaskManagement: state => {
|
||||
@@ -754,10 +780,7 @@ const taskManagementSlice = createSlice({
|
||||
task.show_sub_tasks = !task.show_sub_tasks;
|
||||
}
|
||||
},
|
||||
addSubtaskToParent: (
|
||||
state,
|
||||
action: PayloadAction<{ parentId: string; subtask: Task }>
|
||||
) => {
|
||||
addSubtaskToParent: (state, action: PayloadAction<{ parentId: string; subtask: Task }>) => {
|
||||
const { parentId, subtask } = action.payload;
|
||||
const parent = state.entities[parentId];
|
||||
if (parent) {
|
||||
@@ -808,7 +831,7 @@ const taskManagementSlice = createSlice({
|
||||
show_sub_tasks: false,
|
||||
isTemporary: true, // Mark as temporary
|
||||
};
|
||||
|
||||
|
||||
// Add temporary subtask for immediate UI feedback
|
||||
if (!parent.sub_tasks) {
|
||||
parent.sub_tasks = [];
|
||||
@@ -832,14 +855,17 @@ const taskManagementSlice = createSlice({
|
||||
state.ids = state.ids.filter(id => id !== tempId);
|
||||
}
|
||||
},
|
||||
updateTaskAssignees: (state, action: PayloadAction<{
|
||||
taskId: string;
|
||||
assigneeIds: string[];
|
||||
assigneeNames: InlineMember[];
|
||||
}>) => {
|
||||
updateTaskAssignees: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
taskId: string;
|
||||
assigneeIds: string[];
|
||||
assigneeNames: InlineMember[];
|
||||
}>
|
||||
) => {
|
||||
const { taskId, assigneeIds, assigneeNames } = action.payload;
|
||||
const existingTask = state.entities[taskId];
|
||||
|
||||
|
||||
if (existingTask) {
|
||||
state.entities[taskId] = {
|
||||
...existingTask,
|
||||
@@ -893,23 +919,26 @@ const taskManagementSlice = createSlice({
|
||||
if (field) {
|
||||
return {
|
||||
...column,
|
||||
pinned: field.visible
|
||||
pinned: field.visible,
|
||||
};
|
||||
}
|
||||
return column;
|
||||
});
|
||||
},
|
||||
// Add action to update task counts (comments, attachments, etc.)
|
||||
updateTaskCounts: (state, action: PayloadAction<{
|
||||
taskId: string;
|
||||
counts: {
|
||||
comments_count?: number;
|
||||
attachments_count?: number;
|
||||
has_subscribers?: boolean;
|
||||
has_dependencies?: boolean;
|
||||
schedule_id?: string | null; // Add schedule_id for recurring tasks
|
||||
};
|
||||
}>) => {
|
||||
updateTaskCounts: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
taskId: string;
|
||||
counts: {
|
||||
comments_count?: number;
|
||||
attachments_count?: number;
|
||||
has_subscribers?: boolean;
|
||||
has_dependencies?: boolean;
|
||||
schedule_id?: string | null; // Add schedule_id for recurring tasks
|
||||
};
|
||||
}>
|
||||
) => {
|
||||
const { taskId, counts } = action.payload;
|
||||
const task = state.entities[taskId];
|
||||
if (task) {
|
||||
@@ -941,7 +970,7 @@ const taskManagementSlice = createSlice({
|
||||
.addCase(fetchTasksV3.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
const { allTasks, groups, grouping } = action.payload;
|
||||
|
||||
|
||||
// Preserve existing timer state from old tasks before replacing
|
||||
const oldTasks = state.entities;
|
||||
const tasksWithTimers = (allTasks || []).map(task => {
|
||||
@@ -952,13 +981,13 @@ const taskManagementSlice = createSlice({
|
||||
...task,
|
||||
timeTracking: {
|
||||
...task.timeTracking,
|
||||
activeTimer: oldTask.timeTracking.activeTimer
|
||||
}
|
||||
activeTimer: oldTask.timeTracking.activeTimer,
|
||||
},
|
||||
};
|
||||
}
|
||||
return task;
|
||||
});
|
||||
|
||||
|
||||
tasksAdapter.setAll(state as EntityState<Task, string>, tasksWithTimers); // Ensure allTasks is an array
|
||||
state.ids = tasksWithTimers.map(task => task.id); // Also update ids
|
||||
state.groups = groups;
|
||||
@@ -966,7 +995,8 @@ const taskManagementSlice = createSlice({
|
||||
})
|
||||
.addCase(fetchTasksV3.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.error?.message || (action.payload as string) || 'Failed to load tasks (V3)';
|
||||
state.error =
|
||||
action.error?.message || (action.payload as string) || 'Failed to load tasks (V3)';
|
||||
state.ids = [];
|
||||
state.entities = {};
|
||||
state.groups = [];
|
||||
@@ -1018,7 +1048,7 @@ const taskManagementSlice = createSlice({
|
||||
// Update parent task with subtasks
|
||||
parentTask.sub_tasks = convertedSubtasks;
|
||||
parentTask.sub_tasks_count = convertedSubtasks.length;
|
||||
|
||||
|
||||
// Add subtasks to entities so they can be accessed by ID
|
||||
convertedSubtasks.forEach(subtask => {
|
||||
state.entities[subtask.id] = subtask;
|
||||
@@ -1032,9 +1062,10 @@ const taskManagementSlice = createSlice({
|
||||
// Clear loading state and set error
|
||||
const { taskId } = action.meta.arg;
|
||||
state.loadingSubtasks[taskId] = false;
|
||||
state.error = action.error.message || action.payload || 'Failed to fetch subtasks. Please try again.';
|
||||
state.error =
|
||||
action.error.message || action.payload || 'Failed to fetch subtasks. Please try again.';
|
||||
})
|
||||
.addCase(fetchTasks.pending, (state) => {
|
||||
.addCase(fetchTasks.pending, state => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
@@ -1148,19 +1179,21 @@ export const {
|
||||
export const selectAllTasks = (state: RootState) => state.taskManagement.entities;
|
||||
|
||||
// Memoized selector to prevent unnecessary re-renders
|
||||
export const selectAllTasksArray = createSelector(
|
||||
[selectAllTasks],
|
||||
(entities) => Object.values(entities)
|
||||
export const selectAllTasksArray = createSelector([selectAllTasks], entities =>
|
||||
Object.values(entities)
|
||||
);
|
||||
export const selectTaskById = (state: RootState, taskId: string) => state.taskManagement.entities[taskId];
|
||||
export const selectTaskById = (state: RootState, taskId: string) =>
|
||||
state.taskManagement.entities[taskId];
|
||||
export const selectTaskIds = (state: RootState) => state.taskManagement.ids;
|
||||
export const selectGroups = (state: RootState) => state.taskManagement.groups;
|
||||
export const selectGrouping = (state: RootState) => state.taskManagement.grouping;
|
||||
export const selectLoading = (state: RootState) => state.taskManagement.loading;
|
||||
export const selectError = (state: RootState) => state.taskManagement.error;
|
||||
export const selectSelectedPriorities = (state: RootState) => state.taskManagement.selectedPriorities;
|
||||
export const selectSelectedPriorities = (state: RootState) =>
|
||||
state.taskManagement.selectedPriorities;
|
||||
export const selectSearch = (state: RootState) => state.taskManagement.search;
|
||||
export const selectSubtaskLoading = (state: RootState, taskId: string) => state.taskManagement.loadingSubtasks[taskId] || false;
|
||||
export const selectSubtaskLoading = (state: RootState, taskId: string) =>
|
||||
state.taskManagement.loadingSubtasks[taskId] || false;
|
||||
|
||||
// Memoized selectors to prevent unnecessary re-renders
|
||||
export const selectTasksByStatus = createSelector(
|
||||
@@ -1197,9 +1230,9 @@ export const selectLoadingColumns = (state: RootState) => state.taskManagement.l
|
||||
export const selectColumnsInSync = (state: RootState) => {
|
||||
const columns = state.taskManagement.columns;
|
||||
const fields = state.taskManagementFields || [];
|
||||
|
||||
|
||||
if (columns.length === 0 || fields.length === 0) return true;
|
||||
|
||||
|
||||
return !fields.some(field => {
|
||||
const backendColumn = columns.find(c => c.key === field.key);
|
||||
if (backendColumn) {
|
||||
|
||||
@@ -55,11 +55,16 @@ function saveFields(fields: TaskListField[]) {
|
||||
export const syncFieldWithDatabase = createAsyncThunk(
|
||||
'taskManagementFields/syncFieldWithDatabase',
|
||||
async (
|
||||
{ projectId, fieldKey, visible, columns }: {
|
||||
projectId: string;
|
||||
fieldKey: string;
|
||||
visible: boolean;
|
||||
columns: ITaskListColumn[]
|
||||
{
|
||||
projectId,
|
||||
fieldKey,
|
||||
visible,
|
||||
columns,
|
||||
}: {
|
||||
projectId: string;
|
||||
fieldKey: string;
|
||||
visible: boolean;
|
||||
columns: ITaskListColumn[];
|
||||
},
|
||||
{ dispatch }
|
||||
) => {
|
||||
@@ -67,13 +72,15 @@ export const syncFieldWithDatabase = createAsyncThunk(
|
||||
const backendColumn = columns.find(c => c.key === fieldKey);
|
||||
if (backendColumn) {
|
||||
// Update the column visibility in the database
|
||||
await dispatch(updateColumnVisibility({
|
||||
projectId,
|
||||
item: {
|
||||
...backendColumn,
|
||||
pinned: visible
|
||||
}
|
||||
}));
|
||||
await dispatch(
|
||||
updateColumnVisibility({
|
||||
projectId,
|
||||
item: {
|
||||
...backendColumn,
|
||||
pinned: visible,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
return { fieldKey, visible };
|
||||
}
|
||||
@@ -83,10 +90,14 @@ export const syncFieldWithDatabase = createAsyncThunk(
|
||||
export const syncAllFieldsWithDatabase = createAsyncThunk(
|
||||
'taskManagementFields/syncAllFieldsWithDatabase',
|
||||
async (
|
||||
{ projectId, fields, columns }: {
|
||||
projectId: string;
|
||||
fields: TaskListField[];
|
||||
columns: ITaskListColumn[]
|
||||
{
|
||||
projectId,
|
||||
fields,
|
||||
columns,
|
||||
}: {
|
||||
projectId: string;
|
||||
fields: TaskListField[];
|
||||
columns: ITaskListColumn[];
|
||||
},
|
||||
{ dispatch }
|
||||
) => {
|
||||
@@ -100,13 +111,15 @@ export const syncAllFieldsWithDatabase = createAsyncThunk(
|
||||
const syncPromises = fieldsToSync.map(field => {
|
||||
const backendColumn = columns.find(c => c.key === field.key);
|
||||
if (backendColumn) {
|
||||
return dispatch(updateColumnVisibility({
|
||||
projectId,
|
||||
item: {
|
||||
...backendColumn,
|
||||
pinned: field.visible
|
||||
}
|
||||
}));
|
||||
return dispatch(
|
||||
updateColumnVisibility({
|
||||
projectId,
|
||||
item: {
|
||||
...backendColumn,
|
||||
pinned: field.visible,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
@@ -143,7 +156,10 @@ const taskListFieldsSlice = createSlice({
|
||||
return defaultFields;
|
||||
},
|
||||
// New action to update field visibility from database
|
||||
updateFieldVisibilityFromDatabase(state, action: PayloadAction<{ fieldKey: string; visible: boolean }>) {
|
||||
updateFieldVisibilityFromDatabase(
|
||||
state,
|
||||
action: PayloadAction<{ fieldKey: string; visible: boolean }>
|
||||
) {
|
||||
const { fieldKey, visible } = action.payload;
|
||||
const field = state.find(f => f.key === fieldKey);
|
||||
if (field) {
|
||||
@@ -153,7 +169,7 @@ const taskListFieldsSlice = createSlice({
|
||||
}
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
extraReducers: builder => {
|
||||
builder
|
||||
.addCase(syncFieldWithDatabase.fulfilled, (state, action) => {
|
||||
// Field visibility has been synced with database
|
||||
@@ -177,7 +193,8 @@ const taskListFieldsSlice = createSlice({
|
||||
},
|
||||
});
|
||||
|
||||
export const { toggleField, setFields, resetFields, updateFieldVisibilityFromDatabase } = taskListFieldsSlice.actions;
|
||||
export const { toggleField, setFields, resetFields, updateFieldVisibilityFromDatabase } =
|
||||
taskListFieldsSlice.actions;
|
||||
|
||||
// Utility function to force reset fields (can be called from browser console)
|
||||
export const forceResetFields = () => {
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
import { Avatar, Button, Card, Divider, Drawer, Tag, Timeline, Typography } from '@/shared/antd-imports';
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Card,
|
||||
Divider,
|
||||
Drawer,
|
||||
Tag,
|
||||
Timeline,
|
||||
Typography,
|
||||
} from '@/shared/antd-imports';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||
|
||||
Reference in New Issue
Block a user