From ba0af1bb98a376b64a4eb11388dcc5bde495d659 Mon Sep 17 00:00:00 2001 From: proohit Date: Fri, 25 Mar 2022 16:07:24 +0100 Subject: [PATCH 1/6] feat: persisted user and widget settings --- backend/src/entity/DashboardSettings.ts | 30 ++------ backend/src/index.ts | 2 + .../1636302462708-AddDashboardSettings.ts | 22 ------ .../1648214448116-DashboardSettings.ts | 28 ++++++++ .../controllers/SettingsController.ts | 21 ++++++ backend/src/settings/models/Settings.ts | 28 ++++++++ .../src/settings/models/SettingsController.ts | 7 ++ .../src/settings/services/SettingsService.ts | 18 +++++ .../src/settings/services/settingsRouter.ts | 9 +++ .../repositories/authenticationMapper.ts | 2 + backend/src/shared/repositories/database.ts | 2 + backend/src/shared/services/services.ts | 2 + backend/src/user/services/UserService.ts | 4 ++ frontend/pages/home/index.tsx | 48 ++++++------- .../dashboard/components/LoadingWidget.tsx | 45 ++++++++++++ .../dashboard/components/LoadingWidgets.tsx | 72 +++++++++++++++++++ frontend/src/dashboard/components/Widget.tsx | 20 ++++-- .../src/settings/hooks/useSettingsQuery.ts | 21 ++++++ frontend/src/settings/models/UserSettings.ts | 5 ++ frontend/src/settings/services/SettingsApi.ts | 7 ++ frontend/src/shared/constants/ApiRoutes.ts | 1 + 21 files changed, 314 insertions(+), 80 deletions(-) delete mode 100644 backend/src/migration/1636302462708-AddDashboardSettings.ts create mode 100644 backend/src/migration/1648214448116-DashboardSettings.ts create mode 100644 backend/src/settings/controllers/SettingsController.ts create mode 100644 backend/src/settings/models/Settings.ts create mode 100644 backend/src/settings/models/SettingsController.ts create mode 100644 backend/src/settings/services/SettingsService.ts create mode 100644 backend/src/settings/services/settingsRouter.ts create mode 100644 frontend/src/dashboard/components/LoadingWidget.tsx create mode 100644 frontend/src/dashboard/components/LoadingWidgets.tsx create mode 100644 frontend/src/settings/hooks/useSettingsQuery.ts create mode 100644 frontend/src/settings/models/UserSettings.ts diff --git a/backend/src/entity/DashboardSettings.ts b/backend/src/entity/DashboardSettings.ts index 44a6cd78..8cd9ae83 100644 --- a/backend/src/entity/DashboardSettings.ts +++ b/backend/src/entity/DashboardSettings.ts @@ -1,35 +1,17 @@ -import { Column, Entity, JoinColumn, OneToOne, PrimaryGeneratedColumn } from 'typeorm'; +import { Column, Entity, JoinColumn, ManyToOne, OneToOne, PrimaryGeneratedColumn } from 'typeorm'; import { User } from './User'; -export enum DashboardWidgets { - THIS_MOTH = 'THIS_MONTH', - THIS_YEAR = 'THIS_YEAR', - CURRENT_STATUS = 'CURRENT_STATUS', - LATEST_RECORDS = 'LATEST_RECORDS', - MONTHLY_CATEGORY = 'MONTHLY_CATEGORY', - QUICK_ACTIONS = 'QUICK_ACTIONS', -} - @Entity() export class DashboardSettings { @PrimaryGeneratedColumn('uuid') id: string; - @Column({ - type: 'simple-array', - enum: DashboardWidgets, - default: [ - DashboardWidgets.THIS_MOTH, - DashboardWidgets.THIS_YEAR, - DashboardWidgets.CURRENT_STATUS, - DashboardWidgets.LATEST_RECORDS, - DashboardWidgets.MONTHLY_CATEGORY, - DashboardWidgets.QUICK_ACTIONS, - ], - }) - enabledWidgets: DashboardWidgets[]; + @Column() + widget: string; + @Column() + order: number; @Column() ownerUsername: string; - @OneToOne(() => User) + @ManyToOne(() => User) @JoinColumn({ name: 'ownerUsername' }) owner: User; } diff --git a/backend/src/index.ts b/backend/src/index.ts index f88d6a76..ee694973 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -27,6 +27,7 @@ app.keys = [config.secret]; app.use(session(app)); import passport from 'koa-passport'; +import settingsRouter from './settings/services/settingsRouter'; app.use(passport.initialize()); app.use(passport.session()); @@ -60,6 +61,7 @@ router.use('/records', recordRouter); router.use('/wallets', walletRouter); router.use('/categories', categoryRouter); router.use('/statistics', statisticsRouter); +router.use('/settings', settingsRouter); app.use(router.allowedMethods({ throw: true })); app.use(router.routes()); diff --git a/backend/src/migration/1636302462708-AddDashboardSettings.ts b/backend/src/migration/1636302462708-AddDashboardSettings.ts deleted file mode 100644 index 36b8fd14..00000000 --- a/backend/src/migration/1636302462708-AddDashboardSettings.ts +++ /dev/null @@ -1,22 +0,0 @@ -// import { MigrationInterface, QueryRunner } from 'typeorm'; - -// export class AddDashboardSettings1636302462708 implements MigrationInterface { -// name = 'AddDashboardSettings1636302462708'; - -// public async up(queryRunner: QueryRunner): Promise { -// await queryRunner.query( -// `CREATE TABLE \`dashboard_settings\` (\`id\` char(36) NOT NULL, \`enabledWidgets\` text ('THIS_MONTH', 'THIS_YEAR', 'CURRENT_STATUS', 'LATEST_RECORDS', 'MONTHLY_CATEGORY', 'QUICK_ACTIONS') NOT NULL DEFAULT THIS_MONTH,THIS_YEAR,CURRENT_STATUS,LATEST_RECORDS,MONTHLY_CATEGORY,QUICK_ACTIONS, \`ownerUsername\` varchar(255) NOT NULL, UNIQUE INDEX \`REL_44eca2a73ba5b456b2a9509e39\` (\`ownerUsername\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`, -// ); -// await queryRunner.query( -// `ALTER TABLE \`dashboard_settings\` ADD CONSTRAINT \`FK_44eca2a73ba5b456b2a9509e390\` FOREIGN KEY (\`ownerUsername\`) REFERENCES \`user\`(\`username\`) ON DELETE NO ACTION ON UPDATE NO ACTION`, -// ); -// } - -// public async down(queryRunner: QueryRunner): Promise { -// await queryRunner.query( -// `ALTER TABLE \`dashboard_settings\` DROP FOREIGN KEY \`FK_44eca2a73ba5b456b2a9509e390\``, -// ); -// await queryRunner.query(`DROP INDEX \`REL_44eca2a73ba5b456b2a9509e39\` ON \`dashboard_settings\``); -// await queryRunner.query(`DROP TABLE \`dashboard_settings\``); -// } -// } diff --git a/backend/src/migration/1648214448116-DashboardSettings.ts b/backend/src/migration/1648214448116-DashboardSettings.ts new file mode 100644 index 00000000..6056167b --- /dev/null +++ b/backend/src/migration/1648214448116-DashboardSettings.ts @@ -0,0 +1,28 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; +import { DEFAULT_WIDGETS } from '../settings/models/Settings'; +import { services } from '../shared/services/services'; + +export class DashboardSettings1648214448116 implements MigrationInterface { + name = 'DashboardSettings1648214448116'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE \`dashboard_settings\` (\`id\` varchar(36) NOT NULL, \`widget\` varchar(255) NOT NULL, \`order\` int NOT NULL, \`ownerUsername\` varchar(255) NOT NULL, PRIMARY KEY (\`id\`)) ENGINE=InnoDB`, + ); + await queryRunner.query( + `ALTER TABLE \`dashboard_settings\` ADD CONSTRAINT \`FK_44eca2a73ba5b456b2a9509e390\` FOREIGN KEY (\`ownerUsername\`) REFERENCES \`user\`(\`username\`) ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + + const users = await services.userService.getAllUsers(); + for (const user of users) { + await services.settingsService.updateWidgets(user.username, DEFAULT_WIDGETS); + } + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE \`dashboard_settings\` DROP FOREIGN KEY \`FK_44eca2a73ba5b456b2a9509e390\``, + ); + await queryRunner.query(`DROP TABLE \`dashboard_settings\``); + } +} diff --git a/backend/src/settings/controllers/SettingsController.ts b/backend/src/settings/controllers/SettingsController.ts new file mode 100644 index 00000000..e48bf3c5 --- /dev/null +++ b/backend/src/settings/controllers/SettingsController.ts @@ -0,0 +1,21 @@ +import { services } from '../../shared/services/services'; +import { AvailableWidgets, UserSettings } from '../models/Settings'; +import { SettingsController } from '../models/SettingsController'; + +const SETTINGS_CONTROLLER: SettingsController = { + getUserSettings: async (ctx) => { + const settings = await services.settingsService.getUserWidgets(ctx.state.user.username); + const userSettings: UserSettings = { widgets: settings.map((setting) => setting.widget as AvailableWidgets) }; + return { data: userSettings, status: 200 }; + }, + updateSettings: async (ctx) => { + const { widgets } = ctx.request.body; + const updatedSettings = await services.settingsService.updateWidgets(ctx.state.user.username, widgets); + const updatedUserSettings: UserSettings = { + widgets: updatedSettings.map((setting) => setting.widget), + }; + return { data: updatedUserSettings, status: 200 }; + }, +}; + +export default SETTINGS_CONTROLLER; diff --git a/backend/src/settings/models/Settings.ts b/backend/src/settings/models/Settings.ts new file mode 100644 index 00000000..9172a1ad --- /dev/null +++ b/backend/src/settings/models/Settings.ts @@ -0,0 +1,28 @@ +export const DEFAULT_WIDGETS: AvailableWidgets[] = [ + 'general-header', + 'quick-actions', + 'month-picker', + 'overview-header', + 'month-status', + 'category-data', + 'current-status', + 'daily-records', + 'historical-data-header', + 'latest-records', + 'this-year', +]; + +export type AvailableWidgets = + | 'general-header' + | 'quick-actions' + | 'month-picker' + | 'overview-header' + | 'month-status' + | 'category-data' + | 'current-status' + | 'daily-records' + | 'historical-data-header' + | 'latest-records' + | 'this-year'; + +export type UserSettings = { widgets: AvailableWidgets[] }; diff --git a/backend/src/settings/models/SettingsController.ts b/backend/src/settings/models/SettingsController.ts new file mode 100644 index 00000000..0e726eb0 --- /dev/null +++ b/backend/src/settings/models/SettingsController.ts @@ -0,0 +1,7 @@ +import { ControllerFunction } from '../../shared/models/BaseController'; +import { UserSettings } from './Settings'; + +export interface SettingsController { + getUserSettings: ControllerFunction; + updateSettings: ControllerFunction; +} diff --git a/backend/src/settings/services/SettingsService.ts b/backend/src/settings/services/SettingsService.ts new file mode 100644 index 00000000..bc89a59e --- /dev/null +++ b/backend/src/settings/services/SettingsService.ts @@ -0,0 +1,18 @@ +import { User } from '../../entity/User'; +import { repositories } from '../../shared/repositories/database'; +import { AvailableWidgets } from '../models/Settings'; + +export default class SettingsService { + getUserWidgets(username: User['username']) { + return repositories.settings().find({ where: { ownerUsername: username }, order: { order: 'ASC' } }); + } + async updateWidgets(username: User['username'], widgets: AvailableWidgets[]) { + const newWidgetOrder = widgets.map((widget, index) => ({ + widget, + order: index, + ownerUsername: username, + })); + await repositories.settings().delete({ ownerUsername: username }); + return repositories.settings().save(newWidgetOrder); + } +} diff --git a/backend/src/settings/services/settingsRouter.ts b/backend/src/settings/services/settingsRouter.ts new file mode 100644 index 00000000..c0c1ebb1 --- /dev/null +++ b/backend/src/settings/services/settingsRouter.ts @@ -0,0 +1,9 @@ +import Router from '@koa/router'; +import SETTINGS_CONTROLLER from '../controllers/SettingsController'; + +const router = new Router(); + +router.get('/', SETTINGS_CONTROLLER.getUserSettings); +router.put('/', SETTINGS_CONTROLLER.updateSettings); + +export default router.routes(); diff --git a/backend/src/shared/repositories/authenticationMapper.ts b/backend/src/shared/repositories/authenticationMapper.ts index dd0110eb..4bb1af81 100644 --- a/backend/src/shared/repositories/authenticationMapper.ts +++ b/backend/src/shared/repositories/authenticationMapper.ts @@ -1,5 +1,6 @@ import passport from 'koa-passport'; import { Strategy as LocalStrategy } from 'passport-local'; +import { DEFAULT_WIDGETS } from '../../settings/models/Settings'; import { DuplicatedUser } from '../../user/models/Errors'; import { BadRequest, InvalidCredentials, MissingProperty } from '../models/Errors'; import { LoginToken } from '../models/Login'; @@ -81,5 +82,6 @@ export const register = async (username: string, password: string, email: string const newUser = await repositories .users() .save({ username, password: encryptedPassword, private_key: privateKey, email }); + await services.settingsService.updateWidgets(newUser.username, DEFAULT_WIDGETS); return { username: newUser.username, email: newUser.email }; }; diff --git a/backend/src/shared/repositories/database.ts b/backend/src/shared/repositories/database.ts index dd2ee144..cffad0c5 100644 --- a/backend/src/shared/repositories/database.ts +++ b/backend/src/shared/repositories/database.ts @@ -4,6 +4,7 @@ import { User } from '../../entity/User'; import { CategoryRepository } from '../../category/repositories/CategoryRepository'; import { WalletRepository } from '../../wallet/repositories/WalletRepository'; import { RecurrentRecord } from '../../entity/RecurrentRecord'; +import { DashboardSettings } from '../../entity/DashboardSettings'; export const connection = createConnection(); @@ -13,4 +14,5 @@ export const repositories = { categories: () => getCustomRepository(CategoryRepository), users: () => getRepository(User), recurrentRecords: () => getRepository(RecurrentRecord), + settings: () => getRepository(DashboardSettings), }; diff --git a/backend/src/shared/services/services.ts b/backend/src/shared/services/services.ts index 2d9759dd..c602a989 100644 --- a/backend/src/shared/services/services.ts +++ b/backend/src/shared/services/services.ts @@ -1,6 +1,7 @@ import { CategoryService } from '../../category/services/CategoryService'; import { RecordService } from '../../record/services/RecordService'; import { RecurrentRecordService } from '../../record/services/RecurrentRecordService'; +import SettingsService from '../../settings/services/SettingsService'; import { StatisticsService } from '../../statistics/services/StatisticsService'; import UserService from '../../user/services/UserService'; import WalletService from '../../wallet/services/WalletService'; @@ -14,4 +15,5 @@ export const services = { userService: new UserService(), statisticsService: new StatisticsService(), authenticationService: new AuthenticationService(), + settingsService: new SettingsService(), }; diff --git a/backend/src/user/services/UserService.ts b/backend/src/user/services/UserService.ts index 66b0b726..6d4bde92 100644 --- a/backend/src/user/services/UserService.ts +++ b/backend/src/user/services/UserService.ts @@ -41,4 +41,8 @@ export default class UserService { repositories.users().save({ ...user, password: encryptedNewPassword }); return 'Successfully updated password'; } + + async getAllUsers() { + return repositories.users().find(); + } } diff --git a/frontend/pages/home/index.tsx b/frontend/pages/home/index.tsx index b515bdca..d3ea8fd8 100644 --- a/frontend/pages/home/index.tsx +++ b/frontend/pages/home/index.tsx @@ -1,56 +1,44 @@ import { Grid } from '@mui/material'; import dayjs from 'dayjs'; import Head from 'next/head'; -import * as React from 'react'; +import { FunctionComponent, useState } from 'react'; import { CategoryDataWidget } from '../../src/dashboard/components/CategoryDataWidget'; import { CurrentStatusWidget } from '../../src/dashboard/components/CurrentStatusWidget'; import { DailyDataWidget } from '../../src/dashboard/components/DailyDataWidget'; import { GeneralHeaderWidget } from '../../src/dashboard/components/GeneralHeaderWidget'; import { HistoricalDataHeaderWidget } from '../../src/dashboard/components/HistoricalDataHeaderWidget'; import { LatestRecordsWidget } from '../../src/dashboard/components/LatestRecordsWidget'; +import { LoadingWidgets } from '../../src/dashboard/components/LoadingWidgets'; import { MonthPickerWidget } from '../../src/dashboard/components/MonthPickerWidget'; import { MonthStatusWidget } from '../../src/dashboard/components/MonthStatusWidget'; import { OverviewHeaderWidget } from '../../src/dashboard/components/OverviewHeaderWidget'; import { QuickActionsWidget } from '../../src/dashboard/components/QuickActionsWidget'; import { ThisYearWidget } from '../../src/dashboard/components/ThisYearWidget'; import { AvailableWidgets } from '../../src/dashboard/models/AvailableWidgets'; +import { + useUpdateUserSettingsMutation, + useUserSettingsQuery, +} from '../../src/settings/hooks/useSettingsQuery'; import { useWalletsQuery } from '../../src/wallets/hooks/walletsQueries'; -const defaultWidgetIdsOrder: AvailableWidgets[] = [ - 'general-header', - 'quick-actions', - 'month-picker', - 'overview-header', - 'month-status', - 'category-data', - 'current-status', - 'daily-records', - 'historical-data-header', - 'latest-records', - 'this-year', -]; - -const DashboardPage: React.FunctionComponent = (props) => { - const [selectedWallet, setSelectedWallet] = React.useState('all'); - const [currentDate, setCurrentDate] = React.useState(dayjs()); +const DashboardPage: FunctionComponent = (props) => { + const [selectedWallet, setSelectedWallet] = useState('all'); + const [currentDate, setCurrentDate] = useState(dayjs()); const { data: wallets } = useWalletsQuery(); - - const [dashboardWidgetsOrder, setDashboardWidgetsOrder] = React.useState< - AvailableWidgets[] - >(defaultWidgetIdsOrder); - - const handleWidgetMove = ( + const { data: dashboardWidgetsOrder, isLoading } = useUserSettingsQuery(); + const updateUserSettings = useUpdateUserSettingsMutation(); + const handleWidgetMove = async ( target: string, event: React.DragEvent ) => { const sourceWidget = event.dataTransfer.types[0]; const targetWidget = target; - const newOrders = [...dashboardWidgetsOrder]; + const newOrders = [...dashboardWidgetsOrder.widgets]; const sourceIndex = newOrders.indexOf(sourceWidget as AvailableWidgets); const targetIndex = newOrders.indexOf(targetWidget as AvailableWidgets); newOrders[sourceIndex] = targetWidget as AvailableWidgets; newOrders[targetIndex] = sourceWidget as AvailableWidgets; - setDashboardWidgetsOrder(newOrders); + await updateUserSettings.mutateAsync({ widgets: newOrders }); }; const getWidgetForWidgetId = (widgetId: AvailableWidgets) => { @@ -136,8 +124,12 @@ const DashboardPage: React.FunctionComponent = (props) => { /> - {dashboardWidgetsOrder.map((widgetId) => - getWidgetForWidgetId(widgetId) + {isLoading ? ( + + ) : ( + dashboardWidgetsOrder.widgets.map((widgetId) => + getWidgetForWidgetId(widgetId) + ) )} diff --git a/frontend/src/dashboard/components/LoadingWidget.tsx b/frontend/src/dashboard/components/LoadingWidget.tsx new file mode 100644 index 00000000..b9a94885 --- /dev/null +++ b/frontend/src/dashboard/components/LoadingWidget.tsx @@ -0,0 +1,45 @@ +import { Box, Grid, GridProps, Paper, Typography } from '@mui/material'; +import makeStyles from '@mui/styles/makeStyles'; +import { FunctionComponent } from 'react'; + +export interface WidgetProps { + title?: string | JSX.Element; + xs?: GridProps['xs']; + lg?: GridProps['lg']; + md?: GridProps['md']; + sm?: GridProps['sm']; + xl?: GridProps['xl']; +} + +const widgetStyle = makeStyles((theme) => ({ + widget: { + padding: theme.spacing(2), + }, +})); + +const LoadingWidget: FunctionComponent = (props) => { + const { children, title, lg, md, sm, xl, xs } = props; + const classes = widgetStyle(); + return ( + + + {title && ( + + {title && ( + + {typeof title === 'string' ? ( + {title} + ) : ( + title + )} + + )} + + )} + {children} + + + ); +}; + +export default LoadingWidget; diff --git a/frontend/src/dashboard/components/LoadingWidgets.tsx b/frontend/src/dashboard/components/LoadingWidgets.tsx new file mode 100644 index 00000000..3f068cee --- /dev/null +++ b/frontend/src/dashboard/components/LoadingWidgets.tsx @@ -0,0 +1,72 @@ +import { Skeleton } from '@mui/material'; +import { FunctionComponent } from 'react'; +import LoadingWidget from './LoadingWidget'; + +export const LoadingWidgets: FunctionComponent = () => ( + <> + + + + } + > + + + } + > + + + + + + } + > + + + } + > + + + +); diff --git a/frontend/src/dashboard/components/Widget.tsx b/frontend/src/dashboard/components/Widget.tsx index a3072259..3ab9e603 100644 --- a/frontend/src/dashboard/components/Widget.tsx +++ b/frontend/src/dashboard/components/Widget.tsx @@ -1,7 +1,15 @@ -import { Box, Collapse, Grid, GridProps, IconButton, Paper, Typography } from '@mui/material'; -import makeStyles from '@mui/styles/makeStyles'; import { ExpandLess, ExpandMore } from '@mui/icons-material'; -import * as React from 'react'; +import { + Box, + Collapse, + Grid, + GridProps, + IconButton, + Paper, + Typography, +} from '@mui/material'; +import makeStyles from '@mui/styles/makeStyles'; +import { FunctionComponent, useState } from 'react'; import { AvailableWidgets } from '../models/AvailableWidgets'; export interface WidgetProps { @@ -29,7 +37,7 @@ const widgetStyle = makeStyles((theme) => ({ }, })); -const Widget: React.FunctionComponent = (props) => { +const Widget: FunctionComponent = (props) => { const { children, title, @@ -44,8 +52,8 @@ const Widget: React.FunctionComponent = (props) => { widgetId, } = props; const classes = widgetStyle(); - const [open, setOpen] = React.useState(!disableClosable); - const [dropTarget, setDropTarget] = React.useState(false); + const [open, setOpen] = useState(!disableClosable); + const [dropTarget, setDropTarget] = useState(false); return ( { + return useQuery( + ['getUserSettings'], + () => settingsApi.getUserSettings(), + { staleTime: 15000 } + ); +}; + +export const useUpdateUserSettingsMutation = () => { + const queryClient = useQueryClient(); + return useMutation( + (settings: UserSettings) => settingsApi.updateSettings(settings), + { onSuccess: () => queryClient.invalidateQueries('getUserSettings') } + ); +}; diff --git a/frontend/src/settings/models/UserSettings.ts b/frontend/src/settings/models/UserSettings.ts new file mode 100644 index 00000000..1f9e8f60 --- /dev/null +++ b/frontend/src/settings/models/UserSettings.ts @@ -0,0 +1,5 @@ +import { AvailableWidgets } from '../../dashboard/models/AvailableWidgets'; + +export interface UserSettings { + widgets: AvailableWidgets[]; +} diff --git a/frontend/src/settings/services/SettingsApi.ts b/frontend/src/settings/services/SettingsApi.ts index 0f7a469d..c3419c44 100644 --- a/frontend/src/settings/services/SettingsApi.ts +++ b/frontend/src/settings/services/SettingsApi.ts @@ -1,5 +1,6 @@ import { API_ROUTES } from '../../shared/constants/ApiRoutes'; import { BASE_API } from '../../shared/models/Api'; +import { UserSettings } from '../models/UserSettings'; export interface SettingsApi { changePassword(oldPassword: string, newPassword: string): Promise; @@ -12,4 +13,10 @@ export class SettingsApiService implements SettingsApi { newPassword, }); } + getUserSettings(): Promise { + return BASE_API.get(API_ROUTES.SETTINGS); + } + updateSettings(settings: UserSettings): Promise { + return BASE_API.put(API_ROUTES.SETTINGS, settings); + } } diff --git a/frontend/src/shared/constants/ApiRoutes.ts b/frontend/src/shared/constants/ApiRoutes.ts index 2a4ab6ff..cba7d65b 100644 --- a/frontend/src/shared/constants/ApiRoutes.ts +++ b/frontend/src/shared/constants/ApiRoutes.ts @@ -19,4 +19,5 @@ export class API_ROUTES { static readonly STATISTICS = `${API_ROUTES.BASE_ROUTE}/statistics`; static readonly STATISTICS_CATEGORIES = `${API_ROUTES.STATISTICS}/categories`; static readonly STATISTICS_STATUS = `${API_ROUTES.STATISTICS}/month-status`; + static readonly SETTINGS = `${API_ROUTES.BASE_ROUTE}/settings`; } From 62da922bc9f0326d79a281a8bd38534fc0cc2242 Mon Sep 17 00:00:00 2001 From: proohit Date: Sat, 26 Mar 2022 15:53:28 +0100 Subject: [PATCH 2/6] feat: movable widgets using react-dnd --- frontend/package-lock.json | 125 +++++++++++++++++++ frontend/package.json | 2 + frontend/pages/home/index.tsx | 6 +- frontend/src/dashboard/components/Widget.tsx | 122 ++++++++++-------- frontend/src/shared/components/Providers.tsx | 16 ++- 5 files changed, 211 insertions(+), 60 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 75765595..187983a4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -24,6 +24,8 @@ "next": "^12.1.0", "next-pwa": "^5.4.6", "react": "^17.0.2", + "react-dnd": "^15.1.1", + "react-dnd-html5-backend": "^15.1.2", "react-dom": "^17.0.2", "react-query": "^3.34.16", "recharts": "^2.1.9", @@ -2739,6 +2741,21 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@react-dnd/asap": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.0.tgz", + "integrity": "sha512-0XhqJSc6pPoNnf8DhdsPHtUhRzZALVzYMTzRwV4VI6DJNJ/5xxfL9OQUwb8IH5/2x7lSf7nAZrnzUD+16VyOVQ==" + }, + "node_modules/@react-dnd/invariant": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-3.0.0.tgz", + "integrity": "sha512-keberJRIqPX15IK3SWS/iO1t/kGETiL1oczKrDitAaMnQ+kpHf81l3MrRmFjvfqcnApE+izEvwM6GsyoIcpsVA==" + }, + "node_modules/@react-dnd/shallowequal": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-3.0.0.tgz", + "integrity": "sha512-1ELWQdJB2UrCXTKK5cCD9uGLLIwECLIEdttKA255owdpchtXohIjZBTlFJszwYi2ZKe2Do+QvUzsGyGCMNwbdw==" + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -4082,6 +4099,16 @@ "node": ">=8" } }, + "node_modules/dnd-core": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-15.1.1.tgz", + "integrity": "sha512-Mtj/Sltcx7stVXzeDg4g7roTe/AmzRuIf/FYOxX6F8gULbY54w066BlErBOzQfn9RIJ3gAYLGX7wvVvoBSq7ig==", + "dependencies": { + "@react-dnd/asap": "4.0.0", + "@react-dnd/invariant": "3.0.0", + "redux": "^4.1.1" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -6512,6 +6539,43 @@ "node": ">=0.10.0" } }, + "node_modules/react-dnd": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-15.1.1.tgz", + "integrity": "sha512-QLrHtPU08U4c5zop0ANeqrHXaQw2EWLMn8DQoN6/e4eSN/UbB84P49/80Qg0MEF29VLB5vikSoiFh9N8ASNmpQ==", + "dependencies": { + "@react-dnd/invariant": "3.0.0", + "@react-dnd/shallowequal": "3.0.0", + "dnd-core": "15.1.1", + "fast-deep-equal": "^3.1.3", + "hoist-non-react-statics": "^3.3.2" + }, + "peerDependencies": { + "@types/hoist-non-react-statics": ">= 3.3.1", + "@types/node": ">= 12", + "@types/react": ">= 16", + "react": ">= 16.14" + }, + "peerDependenciesMeta": { + "@types/hoist-non-react-statics": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-dnd-html5-backend": { + "version": "15.1.2", + "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-15.1.2.tgz", + "integrity": "sha512-mem9QbutUF+aA2YC1y47G3ECjnYV/sCYKSnu5Jd7cbg3fLMPAwbnTf/JayYdnCH5l3eg9akD9dQt+cD0UdF8QQ==", + "dependencies": { + "dnd-core": "15.1.1" + } + }, "node_modules/react-dom": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", @@ -6701,6 +6765,14 @@ "postcss-value-parser": "^3.3.0" } }, + "node_modules/redux": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.2.tgz", + "integrity": "sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -9873,6 +9945,21 @@ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.4.tgz", "integrity": "sha512-q/ytXxO5NKvyT37pmisQAItCFqA7FD/vNb8dgaJy3/630Fsc+Mz9/9f2SziBoIZ30TJooXyTwZmhi1zjXmObYg==" }, + "@react-dnd/asap": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.0.tgz", + "integrity": "sha512-0XhqJSc6pPoNnf8DhdsPHtUhRzZALVzYMTzRwV4VI6DJNJ/5xxfL9OQUwb8IH5/2x7lSf7nAZrnzUD+16VyOVQ==" + }, + "@react-dnd/invariant": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-3.0.0.tgz", + "integrity": "sha512-keberJRIqPX15IK3SWS/iO1t/kGETiL1oczKrDitAaMnQ+kpHf81l3MrRmFjvfqcnApE+izEvwM6GsyoIcpsVA==" + }, + "@react-dnd/shallowequal": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-3.0.0.tgz", + "integrity": "sha512-1ELWQdJB2UrCXTKK5cCD9uGLLIwECLIEdttKA255owdpchtXohIjZBTlFJszwYi2ZKe2Do+QvUzsGyGCMNwbdw==" + }, "@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -10946,6 +11033,16 @@ "path-type": "^4.0.0" } }, + "dnd-core": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-15.1.1.tgz", + "integrity": "sha512-Mtj/Sltcx7stVXzeDg4g7roTe/AmzRuIf/FYOxX6F8gULbY54w066BlErBOzQfn9RIJ3gAYLGX7wvVvoBSq7ig==", + "requires": { + "@react-dnd/asap": "4.0.0", + "@react-dnd/invariant": "3.0.0", + "redux": "^4.1.1" + } + }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -12782,6 +12879,26 @@ "object-assign": "^4.1.1" } }, + "react-dnd": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-15.1.1.tgz", + "integrity": "sha512-QLrHtPU08U4c5zop0ANeqrHXaQw2EWLMn8DQoN6/e4eSN/UbB84P49/80Qg0MEF29VLB5vikSoiFh9N8ASNmpQ==", + "requires": { + "@react-dnd/invariant": "3.0.0", + "@react-dnd/shallowequal": "3.0.0", + "dnd-core": "15.1.1", + "fast-deep-equal": "^3.1.3", + "hoist-non-react-statics": "^3.3.2" + } + }, + "react-dnd-html5-backend": { + "version": "15.1.2", + "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-15.1.2.tgz", + "integrity": "sha512-mem9QbutUF+aA2YC1y47G3ECjnYV/sCYKSnu5Jd7cbg3fLMPAwbnTf/JayYdnCH5l3eg9akD9dQt+cD0UdF8QQ==", + "requires": { + "dnd-core": "15.1.1" + } + }, "react-dom": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", @@ -12922,6 +13039,14 @@ "postcss-value-parser": "^3.3.0" } }, + "redux": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.2.tgz", + "integrity": "sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw==", + "requires": { + "@babel/runtime": "^7.9.2" + } + }, "regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 6cae5fc7..c96d779d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -26,6 +26,8 @@ "next": "^12.1.0", "next-pwa": "^5.4.6", "react": "^17.0.2", + "react-dnd": "^15.1.1", + "react-dnd-html5-backend": "^15.1.2", "react-dom": "^17.0.2", "react-query": "^3.34.16", "recharts": "^2.1.9", diff --git a/frontend/pages/home/index.tsx b/frontend/pages/home/index.tsx index d3ea8fd8..982c842c 100644 --- a/frontend/pages/home/index.tsx +++ b/frontend/pages/home/index.tsx @@ -28,11 +28,9 @@ const DashboardPage: FunctionComponent = (props) => { const { data: dashboardWidgetsOrder, isLoading } = useUserSettingsQuery(); const updateUserSettings = useUpdateUserSettingsMutation(); const handleWidgetMove = async ( - target: string, - event: React.DragEvent + sourceWidget: string, + targetWidget: string ) => { - const sourceWidget = event.dataTransfer.types[0]; - const targetWidget = target; const newOrders = [...dashboardWidgetsOrder.widgets]; const sourceIndex = newOrders.indexOf(sourceWidget as AvailableWidgets); const targetIndex = newOrders.indexOf(targetWidget as AvailableWidgets); diff --git a/frontend/src/dashboard/components/Widget.tsx b/frontend/src/dashboard/components/Widget.tsx index 3ab9e603..e18dbe04 100644 --- a/frontend/src/dashboard/components/Widget.tsx +++ b/frontend/src/dashboard/components/Widget.tsx @@ -1,4 +1,4 @@ -import { ExpandLess, ExpandMore } from '@mui/icons-material'; +import { DragIndicator, ExpandLess, ExpandMore } from '@mui/icons-material'; import { Box, Collapse, @@ -7,9 +7,11 @@ import { IconButton, Paper, Typography, + useMediaQuery, } from '@mui/material'; -import makeStyles from '@mui/styles/makeStyles'; +import { styled } from '@mui/styles'; import { FunctionComponent, useState } from 'react'; +import { useDrag, useDrop } from 'react-dnd'; import { AvailableWidgets } from '../models/AvailableWidgets'; export interface WidgetProps { @@ -21,22 +23,25 @@ export interface WidgetProps { sm?: GridProps['sm']; xl?: GridProps['xl']; disableClosable?: boolean; - onWidgetDrop: ( - target: string, - event: React.DragEvent - ) => void; + onWidgetDrop: (sourceWidget: string, targetWidget: string) => void; widgetId: AvailableWidgets; } -const widgetStyle = makeStyles((theme) => ({ - widget: { - padding: theme.spacing(2), - }, - dropTarget: { - border: '2px dashed #ccc', - }, +const DragIcon = styled(Grid)(({ theme }) => ({ + cursor: 'move', + padding: theme.spacing(1), + height: theme.spacing(5), + width: theme.spacing(5), })); +type WidgetDragObject = { + widgetId: string; +}; + +type WidgetDropCollectedProps = { + isOver: boolean; +}; + const Widget: FunctionComponent = (props) => { const { children, @@ -51,45 +56,51 @@ const Widget: FunctionComponent = (props) => { onWidgetDrop, widgetId, } = props; - const classes = widgetStyle(); + const [open, setOpen] = useState(!disableClosable); - const [dropTarget, setDropTarget] = useState(false); + const isDesktop = useMediaQuery('(pointer: fine)'); + const [, drag, preview] = useDrag( + () => ({ + type: 'widget', + item: { + widgetId, + }, + options: { + dropEffect: 'move', + }, + }), + [widgetId] + ); + + const [{ isOver }, drop] = useDrop< + WidgetDragObject, + unknown, + WidgetDropCollectedProps + >( + () => ({ + accept: 'widget', + canDrop: (item) => item.widgetId !== widgetId, + drop: (item) => { + onWidgetDrop(widgetId, item.widgetId); + }, + collect: (monitor) => ({ + isOver: !!monitor.isOver() && !!monitor.canDrop(), + }), + }), + [widgetId, onWidgetDrop] + ); + return ( - { - event.dataTransfer.setData(widgetId, ''); - event.dataTransfer.effectAllowed = 'move'; - }} - onDragOver={(event) => { - event.preventDefault(); - const sourceWidgetKey = event.dataTransfer.types[0]; - const targetWidgetKey = widgetId; - if (sourceWidgetKey === targetWidgetKey) { - event.dataTransfer.dropEffect = 'none'; - setDropTarget(false); - } else { - event.dataTransfer.dropEffect = 'move'; - setDropTarget(true); - } - }} - onDragLeave={() => { - setDropTarget(false); - }} - onDrop={(event) => { - setDropTarget(false); - onWidgetDrop(widgetId, event); - }} - item - xs={xs || 12} - lg={lg} - md={md} - xl={xl} - sm={sm} - > + ({ + ...(isOver && { + border: '2px dashed #ccc', + }), + padding: theme.spacing(2), + })} + ref={drop} variant="outlined" - className={`${classes.widget} ${dropTarget && classes.dropTarget}`} > {(title || actions || !disableClosable) && ( @@ -99,16 +110,27 @@ const Widget: FunctionComponent = (props) => { )} {(!disableClosable || actions) && ( - + {open && actions?.map((action, idx) => ( {action} ))} + {isDesktop && ( + + + + )} {!disableClosable && ( - setOpen(!open)} size="large"> + setOpen(!open)}> {open ? : } diff --git a/frontend/src/shared/components/Providers.tsx b/frontend/src/shared/components/Providers.tsx index 03924bc5..f1dc560b 100644 --- a/frontend/src/shared/components/Providers.tsx +++ b/frontend/src/shared/components/Providers.tsx @@ -13,6 +13,8 @@ import { AuthenticationProvider } from '../../authentication/components/Authenti import { AccTheme } from '../globals/styles/AccTheme'; import DialogsProvider from './DialogsProvider'; import NotificationBar from './NotificationBar'; +import { DndProvider } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; declare module '@mui/styles/defaultTheme' { interface DefaultTheme extends Theme {} @@ -29,12 +31,14 @@ const Providers: FunctionComponent = (props) => { - - - - {props.children} - - + + + + + {props.children} + + + From b15f90db654047246b96aff4bc20adb465f3c1c6 Mon Sep 17 00:00:00 2001 From: proohit Date: Sat, 26 Mar 2022 16:14:34 +0100 Subject: [PATCH 3/6] feat: removable widgets --- frontend/pages/home/index.tsx | 31 +++++++- .../components/CategoryDataWidget.tsx | 4 +- .../components/CurrentStatusWidget.tsx | 4 +- .../dashboard/components/DailyDataWidget.tsx | 4 +- .../components/LatestRecordsWidget.tsx | 4 +- .../components/MonthPickerWidget.tsx | 4 +- .../components/MonthStatusWidget.tsx | 4 +- .../components/QuickActionsWidget.tsx | 4 +- .../dashboard/components/ThisYearWidget.tsx | 4 +- frontend/src/dashboard/components/Widget.tsx | 71 +++++++++++-------- .../dashboard/models/EditableWidgetProps.tsx | 11 +++ .../dashboard/models/MovableWidgetProps.tsx | 5 -- 12 files changed, 97 insertions(+), 53 deletions(-) create mode 100644 frontend/src/dashboard/models/EditableWidgetProps.tsx delete mode 100644 frontend/src/dashboard/models/MovableWidgetProps.tsx diff --git a/frontend/pages/home/index.tsx b/frontend/pages/home/index.tsx index 982c842c..ce034a21 100644 --- a/frontend/pages/home/index.tsx +++ b/frontend/pages/home/index.tsx @@ -27,6 +27,7 @@ const DashboardPage: FunctionComponent = (props) => { const { data: wallets } = useWalletsQuery(); const { data: dashboardWidgetsOrder, isLoading } = useUserSettingsQuery(); const updateUserSettings = useUpdateUserSettingsMutation(); + const handleWidgetMove = async ( sourceWidget: string, targetWidget: string @@ -39,6 +40,13 @@ const DashboardPage: FunctionComponent = (props) => { await updateUserSettings.mutateAsync({ widgets: newOrders }); }; + const handleWidgetRemove = async (widget: AvailableWidgets) => { + const newOrders = [...dashboardWidgetsOrder.widgets]; + const index = newOrders.indexOf(widget); + newOrders.splice(index, 1); + await updateUserSettings.mutateAsync({ widgets: newOrders }); + }; + const getWidgetForWidgetId = (widgetId: AvailableWidgets) => { switch (widgetId) { case 'category-data': @@ -47,6 +55,7 @@ const DashboardPage: FunctionComponent = (props) => { key={widgetId} date={currentDate} onWidgetDrop={handleWidgetMove} + onWidgetRemove={handleWidgetRemove} /> ); case 'current-status': @@ -54,6 +63,7 @@ const DashboardPage: FunctionComponent = (props) => { ); @@ -63,6 +73,7 @@ const DashboardPage: FunctionComponent = (props) => { key={widgetId} date={currentDate} onWidgetDrop={handleWidgetMove} + onWidgetRemove={handleWidgetRemove} selectedWallet={selectedWallet} setSelectedWallet={setSelectedWallet} wallets={wallets} @@ -74,7 +85,11 @@ const DashboardPage: FunctionComponent = (props) => { return ; case 'latest-records': return ( - + ); case 'month-picker': return ( @@ -82,6 +97,7 @@ const DashboardPage: FunctionComponent = (props) => { key={widgetId} date={currentDate} onWidgetDrop={handleWidgetMove} + onWidgetRemove={handleWidgetRemove} setCurrentDate={setCurrentDate} /> ); @@ -91,17 +107,26 @@ const DashboardPage: FunctionComponent = (props) => { key={widgetId} date={currentDate} onWidgetDrop={handleWidgetMove} + onWidgetRemove={handleWidgetRemove} /> ); case 'overview-header': return ; case 'quick-actions': return ( - + ); case 'this-year': return ( - + ); } }; diff --git a/frontend/src/dashboard/components/CategoryDataWidget.tsx b/frontend/src/dashboard/components/CategoryDataWidget.tsx index a1363d2a..daad601c 100644 --- a/frontend/src/dashboard/components/CategoryDataWidget.tsx +++ b/frontend/src/dashboard/components/CategoryDataWidget.tsx @@ -2,11 +2,11 @@ import { Dayjs } from 'dayjs'; import * as React from 'react'; import MonthlyCategory from './MonthlyCategory'; import Widget from './Widget'; -import { MovableWidgetProps } from '../models/MovableWidgetProps'; +import { EditableWidgetProps } from '../models/EditableWidgetProps'; import { DateableWidget } from '../models/DateableWidget'; export const CategoryDataWidget: React.FC< - MovableWidgetProps & DateableWidget + EditableWidgetProps & DateableWidget > = (props) => { const { date, ...rest } = props; return ( diff --git a/frontend/src/dashboard/components/CurrentStatusWidget.tsx b/frontend/src/dashboard/components/CurrentStatusWidget.tsx index f1613ef2..98d1cb07 100644 --- a/frontend/src/dashboard/components/CurrentStatusWidget.tsx +++ b/frontend/src/dashboard/components/CurrentStatusWidget.tsx @@ -1,10 +1,10 @@ import { Wallet } from '../../wallets/models/Wallet'; -import { MovableWidgetProps } from '../models/MovableWidgetProps'; +import { EditableWidgetProps } from '../models/EditableWidgetProps'; import CurrentStatus from './CurrentStatus'; import Widget from './Widget'; export const CurrentStatusWidget: React.FC< - MovableWidgetProps & { wallets: Wallet[] } + EditableWidgetProps & { wallets: Wallet[] } > = (props) => { const { wallets, ...rest } = props; return ( diff --git a/frontend/src/dashboard/components/DailyDataWidget.tsx b/frontend/src/dashboard/components/DailyDataWidget.tsx index d45843c9..31cfa9f0 100644 --- a/frontend/src/dashboard/components/DailyDataWidget.tsx +++ b/frontend/src/dashboard/components/DailyDataWidget.tsx @@ -1,12 +1,12 @@ import { WalletField } from '../../records/components/WalletField'; import { Wallet } from '../../wallets/models/Wallet'; import { DateableWidget } from '../models/DateableWidget'; -import { MovableWidgetProps } from '../models/MovableWidgetProps'; +import { EditableWidgetProps } from '../models/EditableWidgetProps'; import ThisMonth from './ThisMonth'; import Widget from './Widget'; export const DailyDataWidget: React.FC< - MovableWidgetProps & + EditableWidgetProps & DateableWidget & { selectedWallet: string; wallets: Wallet[]; diff --git a/frontend/src/dashboard/components/LatestRecordsWidget.tsx b/frontend/src/dashboard/components/LatestRecordsWidget.tsx index 3abd5f20..89b9713c 100644 --- a/frontend/src/dashboard/components/LatestRecordsWidget.tsx +++ b/frontend/src/dashboard/components/LatestRecordsWidget.tsx @@ -1,8 +1,8 @@ import LatestRecords from './LatestRecords'; import Widget from './Widget'; -import { MovableWidgetProps } from '../models/MovableWidgetProps'; +import { EditableWidgetProps } from '../models/EditableWidgetProps'; -export const LatestRecordsWidget: React.FC = (props) => ( +export const LatestRecordsWidget: React.FC = (props) => ( void; } diff --git a/frontend/src/dashboard/components/MonthStatusWidget.tsx b/frontend/src/dashboard/components/MonthStatusWidget.tsx index 859f4480..0eceaa3d 100644 --- a/frontend/src/dashboard/components/MonthStatusWidget.tsx +++ b/frontend/src/dashboard/components/MonthStatusWidget.tsx @@ -1,10 +1,10 @@ import { DateableWidget } from '../models/DateableWidget'; -import { MovableWidgetProps } from '../models/MovableWidgetProps'; +import { EditableWidgetProps } from '../models/EditableWidgetProps'; import MonthStatus from './MonthStatus'; import Widget from './Widget'; export const MonthStatusWidget: React.FC< - MovableWidgetProps & DateableWidget + EditableWidgetProps & DateableWidget > = (props) => { const { date, ...rest } = props; return ( diff --git a/frontend/src/dashboard/components/QuickActionsWidget.tsx b/frontend/src/dashboard/components/QuickActionsWidget.tsx index cb7e067f..df331667 100644 --- a/frontend/src/dashboard/components/QuickActionsWidget.tsx +++ b/frontend/src/dashboard/components/QuickActionsWidget.tsx @@ -1,8 +1,8 @@ -import { MovableWidgetProps } from '../models/MovableWidgetProps'; +import { EditableWidgetProps } from '../models/EditableWidgetProps'; import { QuickActions } from './QuickActions'; import Widget from './Widget'; -export const QuickActionsWidget: React.FC = (props) => ( +export const QuickActionsWidget: React.FC = (props) => ( diff --git a/frontend/src/dashboard/components/ThisYearWidget.tsx b/frontend/src/dashboard/components/ThisYearWidget.tsx index 2595f960..3e17d2ba 100644 --- a/frontend/src/dashboard/components/ThisYearWidget.tsx +++ b/frontend/src/dashboard/components/ThisYearWidget.tsx @@ -1,8 +1,8 @@ -import { MovableWidgetProps } from '../models/MovableWidgetProps'; +import { EditableWidgetProps } from '../models/EditableWidgetProps'; import ThisYear from './ThisYear'; import Widget from './Widget'; -export const ThisYearWidget: React.FC = (props) => ( +export const ThisYearWidget: React.FC = (props) => ( diff --git a/frontend/src/dashboard/components/Widget.tsx b/frontend/src/dashboard/components/Widget.tsx index e18dbe04..736f3db9 100644 --- a/frontend/src/dashboard/components/Widget.tsx +++ b/frontend/src/dashboard/components/Widget.tsx @@ -1,4 +1,9 @@ -import { DragIndicator, ExpandLess, ExpandMore } from '@mui/icons-material'; +import { + Close, + DragIndicator, + ExpandLess, + ExpandMore, +} from '@mui/icons-material'; import { Box, Collapse, @@ -23,8 +28,12 @@ export interface WidgetProps { sm?: GridProps['sm']; xl?: GridProps['xl']; disableClosable?: boolean; - onWidgetDrop: (sourceWidget: string, targetWidget: string) => void; + onWidgetDrop: ( + sourceWidget: AvailableWidgets, + targetWidget: AvailableWidgets + ) => void; widgetId: AvailableWidgets; + onWidgetRemove: (widget: AvailableWidgets) => void; } const DragIcon = styled(Grid)(({ theme }) => ({ @@ -35,7 +44,7 @@ const DragIcon = styled(Grid)(({ theme }) => ({ })); type WidgetDragObject = { - widgetId: string; + widgetId: AvailableWidgets; }; type WidgetDropCollectedProps = { @@ -54,6 +63,7 @@ const Widget: FunctionComponent = (props) => { xs, disableClosable, onWidgetDrop, + onWidgetRemove, widgetId, } = props; @@ -109,34 +119,37 @@ const Widget: FunctionComponent = (props) => { {{title}} )} - {(!disableClosable || actions) && ( - - {open && - actions?.map((action, idx) => ( - - {action} - - ))} - {isDesktop && ( - - - - )} - {!disableClosable && ( - - setOpen(!open)}> - {open ? : } - + + {open && + actions?.map((action, idx) => ( + + {action} - )} + ))} + {isDesktop && ( + + + + )} + + onWidgetRemove(widgetId)}> + + - )} + {!disableClosable && ( + + setOpen(!open)}> + {open ? : } + + + )} + )} diff --git a/frontend/src/dashboard/models/EditableWidgetProps.tsx b/frontend/src/dashboard/models/EditableWidgetProps.tsx new file mode 100644 index 00000000..b73ab5f2 --- /dev/null +++ b/frontend/src/dashboard/models/EditableWidgetProps.tsx @@ -0,0 +1,11 @@ +import { WidgetProps } from '../components/Widget'; + +export type MovableWidgetProps = Partial & { + onWidgetDrop: WidgetProps['onWidgetDrop']; +}; + +export type RemoveableWidgetProps = Partial & { + onWidgetRemove: WidgetProps['onWidgetRemove']; +}; + +export type EditableWidgetProps = MovableWidgetProps & RemoveableWidgetProps; diff --git a/frontend/src/dashboard/models/MovableWidgetProps.tsx b/frontend/src/dashboard/models/MovableWidgetProps.tsx deleted file mode 100644 index 967d6277..00000000 --- a/frontend/src/dashboard/models/MovableWidgetProps.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { WidgetProps } from '../components/Widget'; - -export type MovableWidgetProps = Partial & { - onWidgetDrop: WidgetProps['onWidgetDrop']; -}; From 491b7585ca1c6aac8a7171112fe8ad4ffe41bb9c Mon Sep 17 00:00:00 2001 From: proohit Date: Sat, 26 Mar 2022 18:10:19 +0100 Subject: [PATCH 4/6] feat: addable widgets and refactoring - refactoring includes enum for AvailableWidgets and WidgetHeader --- frontend/pages/home/index.tsx | 139 +++++++++++------- .../components/GeneralHeaderWidget.tsx | 6 - .../components/HistoricalDataHeaderWidget.tsx | 12 -- .../components/OverviewHeaderWidget.tsx | 6 - .../src/dashboard/components/WidgetHeader.tsx | 69 ++++++++- frontend/src/dashboard/constants/widgets.ts | 41 ++++++ .../src/dashboard/models/AvailableWidgets.tsx | 26 ++-- .../dashboard/models/OptionalWidgetProps.tsx | 3 - 8 files changed, 198 insertions(+), 104 deletions(-) delete mode 100644 frontend/src/dashboard/components/GeneralHeaderWidget.tsx delete mode 100644 frontend/src/dashboard/components/HistoricalDataHeaderWidget.tsx delete mode 100644 frontend/src/dashboard/components/OverviewHeaderWidget.tsx create mode 100644 frontend/src/dashboard/constants/widgets.ts delete mode 100644 frontend/src/dashboard/models/OptionalWidgetProps.tsx diff --git a/frontend/pages/home/index.tsx b/frontend/pages/home/index.tsx index ce034a21..71cd6ca8 100644 --- a/frontend/pages/home/index.tsx +++ b/frontend/pages/home/index.tsx @@ -5,15 +5,22 @@ import { FunctionComponent, useState } from 'react'; import { CategoryDataWidget } from '../../src/dashboard/components/CategoryDataWidget'; import { CurrentStatusWidget } from '../../src/dashboard/components/CurrentStatusWidget'; import { DailyDataWidget } from '../../src/dashboard/components/DailyDataWidget'; -import { GeneralHeaderWidget } from '../../src/dashboard/components/GeneralHeaderWidget'; -import { HistoricalDataHeaderWidget } from '../../src/dashboard/components/HistoricalDataHeaderWidget'; import { LatestRecordsWidget } from '../../src/dashboard/components/LatestRecordsWidget'; import { LoadingWidgets } from '../../src/dashboard/components/LoadingWidgets'; import { MonthPickerWidget } from '../../src/dashboard/components/MonthPickerWidget'; import { MonthStatusWidget } from '../../src/dashboard/components/MonthStatusWidget'; -import { OverviewHeaderWidget } from '../../src/dashboard/components/OverviewHeaderWidget'; import { QuickActionsWidget } from '../../src/dashboard/components/QuickActionsWidget'; import { ThisYearWidget } from '../../src/dashboard/components/ThisYearWidget'; +import { WidgetProps } from '../../src/dashboard/components/Widget'; +import { + HeaderWidgetProps, + WidgetHeader, +} from '../../src/dashboard/components/WidgetHeader'; +import { + getHeaderWidgetOfWidget, + headerWidgetMapping, + widgetLabels, +} from '../../src/dashboard/constants/widgets'; import { AvailableWidgets } from '../../src/dashboard/models/AvailableWidgets'; import { useUpdateUserSettingsMutation, @@ -23,20 +30,48 @@ import { useWalletsQuery } from '../../src/wallets/hooks/walletsQueries'; const DashboardPage: FunctionComponent = (props) => { const [selectedWallet, setSelectedWallet] = useState('all'); + const [currentDate, setCurrentDate] = useState(dayjs()); const { data: wallets } = useWalletsQuery(); const { data: dashboardWidgetsOrder, isLoading } = useUserSettingsQuery(); const updateUserSettings = useUpdateUserSettingsMutation(); + const getMissingWidgetsForHeader = ( + headerWidget: AvailableWidgets + ): AvailableWidgets[] => { + const headerWidgets = headerWidgetMapping[headerWidget]; + if (!headerWidgets) { + return []; + } + return headerWidgets.filter( + (widget) => !dashboardWidgetsOrder.widgets.includes(widget) + ); + }; + + const handleWidgetAdd = async (widget: AvailableWidgets) => { + const headerOfWidget = getHeaderWidgetOfWidget(widget); + const indexOfHeader = dashboardWidgetsOrder.widgets.indexOf(headerOfWidget); + if (indexOfHeader >= 0) { + const newWidgets = [ + ...dashboardWidgetsOrder.widgets.slice(0, indexOfHeader + 1), + widget, + ...dashboardWidgetsOrder.widgets.slice(indexOfHeader + 1), + ]; + await updateUserSettings.mutateAsync({ + widgets: newWidgets, + }); + } + }; + const handleWidgetMove = async ( - sourceWidget: string, - targetWidget: string + sourceWidget: AvailableWidgets, + targetWidget: AvailableWidgets ) => { const newOrders = [...dashboardWidgetsOrder.widgets]; - const sourceIndex = newOrders.indexOf(sourceWidget as AvailableWidgets); - const targetIndex = newOrders.indexOf(targetWidget as AvailableWidgets); - newOrders[sourceIndex] = targetWidget as AvailableWidgets; - newOrders[targetIndex] = sourceWidget as AvailableWidgets; + const sourceIndex = newOrders.indexOf(sourceWidget); + const targetIndex = newOrders.indexOf(targetWidget); + newOrders[sourceIndex] = targetWidget; + newOrders[targetIndex] = sourceWidget; await updateUserSettings.mutateAsync({ widgets: newOrders }); }; @@ -48,86 +83,74 @@ const DashboardPage: FunctionComponent = (props) => { }; const getWidgetForWidgetId = (widgetId: AvailableWidgets) => { + const editableWidgetProps: WidgetProps = { + title: widgetLabels[widgetId], + onWidgetDrop: handleWidgetMove, + onWidgetRemove: handleWidgetRemove, + widgetId, + }; + const headerWidgetProps: HeaderWidgetProps = { + title: widgetLabels[widgetId], + widgetAdded: handleWidgetAdd, + addableWidgets: getMissingWidgetsForHeader(widgetId), + }; switch (widgetId) { - case 'category-data': + case AvailableWidgets.CATEGORY_DATA: return ( ); - case 'current-status': + case AvailableWidgets.CURRENT_STATUS: return ( ); - case 'daily-records': + case AvailableWidgets.DAILY_RECORDS: return ( ); - case 'general-header': - return ; - case 'historical-data-header': - return ; - case 'latest-records': - return ( - - ); - case 'month-picker': + case AvailableWidgets.GENERAL_HEADER: + return ; + case AvailableWidgets.HISTORICAL_DATA_HEADER: + return ; + case AvailableWidgets.LATEST_RECORDS: + return ; + case AvailableWidgets.MONTH_PICKER: return ( ); - case 'month-status': + case AvailableWidgets.MONTH_STATUS: return ( - ); - case 'overview-header': - return ; - case 'quick-actions': - return ( - - ); - case 'this-year': - return ( - ); + case AvailableWidgets.OVERVIEW_HEADER: + return ; + case AvailableWidgets.QUICK_ACTIONS: + return ; + case AvailableWidgets.THIS_YEAR: + return ; } }; @@ -150,9 +173,11 @@ const DashboardPage: FunctionComponent = (props) => { {isLoading ? ( ) : ( - dashboardWidgetsOrder.widgets.map((widgetId) => - getWidgetForWidgetId(widgetId) - ) + <> + {dashboardWidgetsOrder.widgets.map((widgetId) => + getWidgetForWidgetId(widgetId) + )} + )} diff --git a/frontend/src/dashboard/components/GeneralHeaderWidget.tsx b/frontend/src/dashboard/components/GeneralHeaderWidget.tsx deleted file mode 100644 index 8451795f..00000000 --- a/frontend/src/dashboard/components/GeneralHeaderWidget.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { OptionalWidgetProps } from '../models/OptionalWidgetProps'; -import { WidgetHeader } from './WidgetHeader'; - -export const GeneralHeaderWidget: React.FC = () => ( - -); diff --git a/frontend/src/dashboard/components/HistoricalDataHeaderWidget.tsx b/frontend/src/dashboard/components/HistoricalDataHeaderWidget.tsx deleted file mode 100644 index 14d1ad3d..00000000 --- a/frontend/src/dashboard/components/HistoricalDataHeaderWidget.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { OptionalWidgetProps } from '../models/OptionalWidgetProps'; -import { WidgetHeader } from './WidgetHeader'; - -export const HistoricalDataHeaderWidget: React.FC = ( - props -) => ( - -); diff --git a/frontend/src/dashboard/components/OverviewHeaderWidget.tsx b/frontend/src/dashboard/components/OverviewHeaderWidget.tsx deleted file mode 100644 index 99693001..00000000 --- a/frontend/src/dashboard/components/OverviewHeaderWidget.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { OptionalWidgetProps } from '../models/OptionalWidgetProps'; -import { WidgetHeader } from './WidgetHeader'; - -export const OverviewHeaderWidget: React.FC = (props) => ( - -); diff --git a/frontend/src/dashboard/components/WidgetHeader.tsx b/frontend/src/dashboard/components/WidgetHeader.tsx index b93cbefd..cd598c1b 100644 --- a/frontend/src/dashboard/components/WidgetHeader.tsx +++ b/frontend/src/dashboard/components/WidgetHeader.tsx @@ -1,18 +1,73 @@ -import { AppBar, Grid, Toolbar, Typography } from '@mui/material'; -import * as React from 'react'; +import { AddBox } from '@mui/icons-material'; +import { + AppBar, + Grid, + IconButton, + Menu, + MenuItem, + Toolbar, + Tooltip, + Typography, +} from '@mui/material'; +import { + FunctionComponent, + MouseEvent as ReactMouseEvent, + useState, +} from 'react'; +import { widgetLabels } from '../constants/widgets'; +import { AvailableWidgets } from '../models/AvailableWidgets'; +import { WidgetProps } from './Widget'; -type WidgetHeaderProps = { - title: string; - widgetId: string; +export type HeaderWidgetProps = Partial & { + addableWidgets: AvailableWidgets[]; + widgetAdded: (widget: AvailableWidgets) => void; }; -export const WidgetHeader: React.FC = (props) => { - const { title } = props; +export const WidgetHeader: FunctionComponent = (props) => { + const { title, widgetAdded, addableWidgets } = props; + const [menuAnchor, setMenuAnchor] = useState(null); + const menuOpen = Boolean(menuAnchor); + + const openMenu = (event: ReactMouseEvent) => { + setMenuAnchor(event.currentTarget); + }; + + const closeMenu = () => { + setMenuAnchor(null); + }; + return ( {title} + + {addableWidgets?.length > 0 && ( + <> + + + + + + + {addableWidgets.map((widgetId) => ( + widgetAdded(widgetId)} + > + {widgetLabels[widgetId]} + + ))} + + + )} + diff --git a/frontend/src/dashboard/constants/widgets.ts b/frontend/src/dashboard/constants/widgets.ts new file mode 100644 index 00000000..5dde454d --- /dev/null +++ b/frontend/src/dashboard/constants/widgets.ts @@ -0,0 +1,41 @@ +import { AvailableWidgets } from '../models/AvailableWidgets'; + +export const widgetLabels: { [key in AvailableWidgets]: string } = { + [AvailableWidgets.GENERAL_HEADER]: 'General', + [AvailableWidgets.QUICK_ACTIONS]: 'Quick Actions', + [AvailableWidgets.MONTH_PICKER]: 'Month Picker', + [AvailableWidgets.OVERVIEW_HEADER]: 'Overview', + [AvailableWidgets.MONTH_STATUS]: 'Month Status', + [AvailableWidgets.HISTORICAL_DATA_HEADER]: 'Historical Data', + [AvailableWidgets.LATEST_RECORDS]: 'Latest Records', + [AvailableWidgets.THIS_YEAR]: 'This Year', + [AvailableWidgets.CATEGORY_DATA]: 'Category Data', + [AvailableWidgets.CURRENT_STATUS]: 'Current Status', + [AvailableWidgets.DAILY_RECORDS]: 'Daily Records', +}; + +export const headerWidgetMapping: { + [key in AvailableWidgets]?: AvailableWidgets[]; +} = { + [AvailableWidgets.GENERAL_HEADER]: [ + AvailableWidgets.QUICK_ACTIONS, + AvailableWidgets.MONTH_PICKER, + ], + [AvailableWidgets.OVERVIEW_HEADER]: [ + AvailableWidgets.MONTH_STATUS, + AvailableWidgets.CATEGORY_DATA, + AvailableWidgets.CURRENT_STATUS, + AvailableWidgets.DAILY_RECORDS, + ], + [AvailableWidgets.HISTORICAL_DATA_HEADER]: [ + AvailableWidgets.LATEST_RECORDS, + AvailableWidgets.THIS_YEAR, + ], +}; + +export const getHeaderWidgetOfWidget = (widget: AvailableWidgets) => { + const headerWidget = Object.keys(headerWidgetMapping).find((key) => + headerWidgetMapping[key].includes(widget) + ); + return headerWidget as AvailableWidgets; +}; diff --git a/frontend/src/dashboard/models/AvailableWidgets.tsx b/frontend/src/dashboard/models/AvailableWidgets.tsx index 1de01fb8..35afc7b0 100644 --- a/frontend/src/dashboard/models/AvailableWidgets.tsx +++ b/frontend/src/dashboard/models/AvailableWidgets.tsx @@ -1,13 +1,13 @@ -export type AvailableWidgets = - | 'general-header' - | 'quick-actions' - | 'month-picker' - | 'overview-header' - | 'month-status' - | 'category-data' - | 'current-status' - | 'daily-records' - | 'historical-data-header' - | 'daily-records' - | 'latest-records' - | 'this-year'; +export enum AvailableWidgets { + GENERAL_HEADER = 'general-header', + QUICK_ACTIONS = 'quick-actions', + MONTH_PICKER = 'month-picker', + OVERVIEW_HEADER = 'overview-header', + MONTH_STATUS = 'month-status', + HISTORICAL_DATA_HEADER = 'historical-data-header', + LATEST_RECORDS = 'latest-records', + THIS_YEAR = 'this-year', + CATEGORY_DATA = 'category-data', + CURRENT_STATUS = 'current-status', + DAILY_RECORDS = 'daily-records', +} diff --git a/frontend/src/dashboard/models/OptionalWidgetProps.tsx b/frontend/src/dashboard/models/OptionalWidgetProps.tsx deleted file mode 100644 index 669356e0..00000000 --- a/frontend/src/dashboard/models/OptionalWidgetProps.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import { WidgetProps } from '../components/Widget'; - -export type OptionalWidgetProps = Partial; From f9a79829d620be24036cfa703d45e538f7fba441 Mon Sep 17 00:00:00 2001 From: proohit Date: Sat, 26 Mar 2022 18:18:46 +0100 Subject: [PATCH 5/6] fix: props errors --- .../src/dashboard/components/CategoryDataWidget.tsx | 3 +-- .../src/dashboard/components/CurrentStatusWidget.tsx | 8 +------- frontend/src/dashboard/components/DailyDataWidget.tsx | 3 +-- .../src/dashboard/components/LatestRecordsWidget.tsx | 8 +------- .../src/dashboard/components/MonthPickerWidget.tsx | 10 +++++----- .../src/dashboard/components/MonthStatusWidget.tsx | 1 - .../src/dashboard/components/QuickActionsWidget.tsx | 2 +- frontend/src/dashboard/components/ThisYearWidget.tsx | 2 +- frontend/src/dashboard/models/EditableWidgetProps.tsx | 4 ++-- 9 files changed, 13 insertions(+), 28 deletions(-) diff --git a/frontend/src/dashboard/components/CategoryDataWidget.tsx b/frontend/src/dashboard/components/CategoryDataWidget.tsx index daad601c..7ae4912c 100644 --- a/frontend/src/dashboard/components/CategoryDataWidget.tsx +++ b/frontend/src/dashboard/components/CategoryDataWidget.tsx @@ -11,11 +11,10 @@ export const CategoryDataWidget: React.FC< const { date, ...rest } = props; return ( diff --git a/frontend/src/dashboard/components/CurrentStatusWidget.tsx b/frontend/src/dashboard/components/CurrentStatusWidget.tsx index 98d1cb07..2c9ffc44 100644 --- a/frontend/src/dashboard/components/CurrentStatusWidget.tsx +++ b/frontend/src/dashboard/components/CurrentStatusWidget.tsx @@ -8,13 +8,7 @@ export const CurrentStatusWidget: React.FC< > = (props) => { const { wallets, ...rest } = props; return ( - + {wallets && } ); diff --git a/frontend/src/dashboard/components/DailyDataWidget.tsx b/frontend/src/dashboard/components/DailyDataWidget.tsx index 31cfa9f0..f815481f 100644 --- a/frontend/src/dashboard/components/DailyDataWidget.tsx +++ b/frontend/src/dashboard/components/DailyDataWidget.tsx @@ -16,6 +16,7 @@ export const DailyDataWidget: React.FC< const { date, selectedWallet, wallets, setSelectedWallet, ...rest } = props; return ( , ]} - widgetId="daily-records" - {...rest} > {wallets && ( <> diff --git a/frontend/src/dashboard/components/LatestRecordsWidget.tsx b/frontend/src/dashboard/components/LatestRecordsWidget.tsx index 89b9713c..ee74367b 100644 --- a/frontend/src/dashboard/components/LatestRecordsWidget.tsx +++ b/frontend/src/dashboard/components/LatestRecordsWidget.tsx @@ -3,13 +3,7 @@ import Widget from './Widget'; import { EditableWidgetProps } from '../models/EditableWidgetProps'; export const LatestRecordsWidget: React.FC = (props) => ( - + ); diff --git a/frontend/src/dashboard/components/MonthPickerWidget.tsx b/frontend/src/dashboard/components/MonthPickerWidget.tsx index a70828e4..378e08ec 100644 --- a/frontend/src/dashboard/components/MonthPickerWidget.tsx +++ b/frontend/src/dashboard/components/MonthPickerWidget.tsx @@ -1,12 +1,12 @@ -import dayjs, { Dayjs } from 'dayjs'; import DatePicker from '@mui/lab/DatePicker'; +import { TextField } from '@mui/material'; +import { Dayjs } from 'dayjs'; +import { FunctionComponent, useState } from 'react'; import { DateableWidget } from '../models/DateableWidget'; import { EditableWidgetProps } from '../models/EditableWidgetProps'; import Widget from './Widget'; -import { TextField } from '@mui/material'; -import { useState } from 'react'; -export const MonthPickerWidget: React.FC< +export const MonthPickerWidget: FunctionComponent< EditableWidgetProps & DateableWidget & { setCurrentDate: (date: Dayjs) => void; @@ -15,7 +15,7 @@ export const MonthPickerWidget: React.FC< const { date, setCurrentDate, ...rest } = props; const [selectedDate, setSelectedDate] = useState(date); return ( - + } views={['year', 'month']} diff --git a/frontend/src/dashboard/components/MonthStatusWidget.tsx b/frontend/src/dashboard/components/MonthStatusWidget.tsx index 0eceaa3d..29d54f8d 100644 --- a/frontend/src/dashboard/components/MonthStatusWidget.tsx +++ b/frontend/src/dashboard/components/MonthStatusWidget.tsx @@ -10,7 +10,6 @@ export const MonthStatusWidget: React.FC< return ( = (props) => ( - + ); diff --git a/frontend/src/dashboard/components/ThisYearWidget.tsx b/frontend/src/dashboard/components/ThisYearWidget.tsx index 3e17d2ba..af1145c6 100644 --- a/frontend/src/dashboard/components/ThisYearWidget.tsx +++ b/frontend/src/dashboard/components/ThisYearWidget.tsx @@ -3,7 +3,7 @@ import ThisYear from './ThisYear'; import Widget from './Widget'; export const ThisYearWidget: React.FC = (props) => ( - + ); diff --git a/frontend/src/dashboard/models/EditableWidgetProps.tsx b/frontend/src/dashboard/models/EditableWidgetProps.tsx index b73ab5f2..4607e838 100644 --- a/frontend/src/dashboard/models/EditableWidgetProps.tsx +++ b/frontend/src/dashboard/models/EditableWidgetProps.tsx @@ -1,10 +1,10 @@ import { WidgetProps } from '../components/Widget'; -export type MovableWidgetProps = Partial & { +export type MovableWidgetProps = WidgetProps & { onWidgetDrop: WidgetProps['onWidgetDrop']; }; -export type RemoveableWidgetProps = Partial & { +export type RemoveableWidgetProps = WidgetProps & { onWidgetRemove: WidgetProps['onWidgetRemove']; }; From 18bd695e14d3aefd274e7368e65ad444a9805a85 Mon Sep 17 00:00:00 2001 From: proohit Date: Sat, 26 Mar 2022 18:53:46 +0100 Subject: [PATCH 6/6] fix: disabled iconbutton style, menu behaviour --- .../src/dashboard/components/WidgetHeader.tsx | 59 +++++++++++-------- .../src/shared/globals/styles/AccTheme.ts | 14 ++++- 2 files changed, 46 insertions(+), 27 deletions(-) diff --git a/frontend/src/dashboard/components/WidgetHeader.tsx b/frontend/src/dashboard/components/WidgetHeader.tsx index cd598c1b..ef7b3590 100644 --- a/frontend/src/dashboard/components/WidgetHeader.tsx +++ b/frontend/src/dashboard/components/WidgetHeader.tsx @@ -13,6 +13,7 @@ import { FunctionComponent, MouseEvent as ReactMouseEvent, useState, + useEffect, } from 'react'; import { widgetLabels } from '../constants/widgets'; import { AvailableWidgets } from '../models/AvailableWidgets'; @@ -26,47 +27,53 @@ export type HeaderWidgetProps = Partial & { export const WidgetHeader: FunctionComponent = (props) => { const { title, widgetAdded, addableWidgets } = props; const [menuAnchor, setMenuAnchor] = useState(null); - const menuOpen = Boolean(menuAnchor); + const [menuOpen, setMenuOpen] = useState(false); const openMenu = (event: ReactMouseEvent) => { + setMenuOpen(true); setMenuAnchor(event.currentTarget); }; const closeMenu = () => { + setMenuOpen(false); setMenuAnchor(null); }; + useEffect(() => { + if (!menuOpen && !!menuAnchor) { + return; + } + addableWidgets?.length === 0 && closeMenu(); + }, [menuOpen, menuAnchor, addableWidgets]); + return ( {title} - {addableWidgets?.length > 0 && ( - <> - - - - - - - {addableWidgets.map((widgetId) => ( - widgetAdded(widgetId)} - > - {widgetLabels[widgetId]} - - ))} - - - )} + + + + + + + {addableWidgets?.map((widgetId) => ( + widgetAdded(widgetId)}> + {widgetLabels[widgetId]} + + ))} + diff --git a/frontend/src/shared/globals/styles/AccTheme.ts b/frontend/src/shared/globals/styles/AccTheme.ts index 71740123..ade3d039 100644 --- a/frontend/src/shared/globals/styles/AccTheme.ts +++ b/frontend/src/shared/globals/styles/AccTheme.ts @@ -1,4 +1,4 @@ -import { createTheme } from '@mui/material'; +import { alpha, createTheme } from '@mui/material'; export const AccTheme = createTheme({ palette: { @@ -67,5 +67,17 @@ export const AccTheme = createTheme({ }), }, }, + MuiIconButton: { + styleOverrides: { + root: ({ theme, ownerState }) => ({ + '&.Mui-disabled': { + color: + ownerState.color === 'primary' + ? alpha(theme.palette.primary.main, 0.5) + : alpha(theme.palette.secondary.main, 0.5), + }, + }), + }, + }, }, });