From 16fbda77c7cea1f153d57a753c92a596d19d08fe Mon Sep 17 00:00:00 2001 From: David Hordiienko Date: Mon, 7 Oct 2024 16:51:40 +0300 Subject: [PATCH 01/28] start to refactor: sheeter & dialoger --- frontend/src/modules/auth/sign-in-form.tsx | 2 +- .../src/modules/common/dialoger/dialog.tsx | 38 ++++++++++++ .../src/modules/common/dialoger/drawer.tsx | 18 ++++++ .../src/modules/common/dialoger/index.tsx | 62 +++---------------- frontend/src/modules/common/dialoger/state.ts | 4 +- frontend/src/modules/common/error-notice.tsx | 2 +- frontend/src/modules/common/main-footer.tsx | 2 +- .../src/modules/common/main-nav/index.tsx | 2 +- .../src/modules/common/sheeter/drawer.tsx | 21 ++----- frontend/src/modules/common/sheeter/index.tsx | 51 +++++---------- frontend/src/modules/common/sheeter/sheet.tsx | 35 +++++------ frontend/src/modules/common/sheeter/state.ts | 33 ++++------ .../src/modules/home/onboarding/footer.tsx | 2 +- .../src/modules/marketing/about/pricing.tsx | 4 +- .../organizations/members-table/index.tsx | 4 +- .../organizations/organization-settings.tsx | 2 +- .../organizations-table/index.tsx | 4 +- .../update-organization-form.tsx | 18 +++--- frontend/src/modules/ui/sheet.tsx | 10 ++- frontend/src/modules/users/settings-page.tsx | 2 +- .../src/modules/users/update-user-form.tsx | 16 +++-- .../src/modules/users/users-table/index.tsx | 4 +- 22 files changed, 159 insertions(+), 177 deletions(-) create mode 100644 frontend/src/modules/common/dialoger/dialog.tsx create mode 100644 frontend/src/modules/common/dialoger/drawer.tsx diff --git a/frontend/src/modules/auth/sign-in-form.tsx b/frontend/src/modules/auth/sign-in-form.tsx index 904426071..6ad691b9f 100644 --- a/frontend/src/modules/auth/sign-in-form.tsx +++ b/frontend/src/modules/auth/sign-in-form.tsx @@ -170,7 +170,7 @@ export const ResetPasswordRequest = ({ email }: { email: string }) => { id: 'send-reset-password', className: 'md:max-w-xl', title: t('common:reset_password'), - text: t('common:reset_password.text'), + description: t('common:reset_password.text'), }, ); }; diff --git a/frontend/src/modules/common/dialoger/dialog.tsx b/frontend/src/modules/common/dialoger/dialog.tsx new file mode 100644 index 000000000..d907d5eee --- /dev/null +++ b/frontend/src/modules/common/dialoger/dialog.tsx @@ -0,0 +1,38 @@ +import type { DialogT } from '~/modules/common/dialoger/state'; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '~/modules/ui/dialog'; + +export interface DialogProp { + dialog: DialogT; + onOpenChange: () => void; +} +export default function StandardDialog({ dialog, onOpenChange }: DialogProp) { + const { id, content, container, description, title, className, containerBackdrop, autoFocus, hideClose } = dialog; + + return ( + + {container && containerBackdrop && ( +
+ )} + { + if (container && !containerBackdrop) e.preventDefault(); + }} + hideClose={hideClose} + onOpenAutoFocus={(event: Event) => { + if (!autoFocus) event.preventDefault(); + }} + className={className} + container={container} + > + + {title} + {description} + + + {/* For accessibility */} + {!description && !title && } + {content} + +
+ ); +} diff --git a/frontend/src/modules/common/dialoger/drawer.tsx b/frontend/src/modules/common/dialoger/drawer.tsx new file mode 100644 index 000000000..94445f1b0 --- /dev/null +++ b/frontend/src/modules/common/dialoger/drawer.tsx @@ -0,0 +1,18 @@ +import type { DialogProp } from '~/modules/common/dialoger/dialog'; +import { Drawer, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle } from '~/modules/ui/drawer'; + +export default function DrawerDialog({ dialog, onOpenChange }: DialogProp) { + const { id, content, open, description, title, className } = dialog; + + return ( + + + + {title} + {description} + +
{content}
+
+
+ ); +} diff --git a/frontend/src/modules/common/dialoger/index.tsx b/frontend/src/modules/common/dialoger/index.tsx index fb5e9a0eb..9b235b1ee 100644 --- a/frontend/src/modules/common/dialoger/index.tsx +++ b/frontend/src/modules/common/dialoger/index.tsx @@ -1,8 +1,8 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { useBreakpoints } from '~/hooks/use-breakpoints'; +import StandardDialog from '~/modules/common/dialoger/dialog'; +import DrawerDialog from '~/modules/common/dialoger/drawer'; import { DialogState, type DialogT, type DialogToRemove } from '~/modules/common/dialoger/state'; -import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '~/modules/ui/dialog'; -import { Drawer, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle } from '~/modules/ui/drawer'; export function Dialoger() { const [dialogs, setDialogs] = useState([]); @@ -12,10 +12,13 @@ export function Dialoger() { const updateDialog = (dialog: DialogT, open: boolean) => { DialogState.update(dialog.id, { open }); + if (!open) { + removeDialog(dialog); + dialog.removeCallback?.(); + } }; const onOpenChange = (dialog: DialogT) => (open: boolean) => { updateDialog(dialog, open); - if (!open) removeDialog(dialog); }; const removeDialog = useCallback((dialog: DialogT | DialogToRemove) => { @@ -59,56 +62,7 @@ export function Dialoger() { return dialogs.map((dialog) => { const existingDialog = updatedDialogs.find(({ id }) => id === dialog.id); - - if (!isMobile || !dialog.drawerOnMobile) { - return ( - - {dialog.container && dialog.containerBackdrop && ( -
- )} - { - if (dialog.container && !dialog.containerBackdrop) e.preventDefault(); - }} - hideClose={dialog.hideClose} - onOpenAutoFocus={(event: Event) => { - if (!dialog.autoFocus) event.preventDefault(); - }} - className={existingDialog?.className ? existingDialog.className : dialog.className} - container={existingDialog?.container ? existingDialog.container : dialog.container} - > - - - {existingDialog?.title - ? existingDialog.title - : dialog.title && (typeof dialog.title === 'string' ? {dialog.title} : dialog.title)} - - {dialog.text} - - - {/* For accessibility */} - {!dialog.text && !dialog.title && } - {existingDialog?.content ? existingDialog.content : dialog.content} - -
- ); - } - - return ( - - - - - {existingDialog?.title - ? existingDialog.title - : dialog.title && (typeof dialog.title === 'string' ? {dialog.title} : dialog.title)} - - {dialog.text} - - -
{dialog.content}
-
-
- ); + const DialogComponent = !isMobile || !dialog.drawerOnMobile ? StandardDialog : DrawerDialog; + return onOpenChange(dialog)} />; }); } diff --git a/frontend/src/modules/common/dialoger/state.ts b/frontend/src/modules/common/dialoger/state.ts index 7da385585..cc2ba3b0d 100644 --- a/frontend/src/modules/common/dialoger/state.ts +++ b/frontend/src/modules/common/dialoger/state.ts @@ -5,7 +5,7 @@ let dialogsCounter = 1; export type DialogT = { id: number | string; title?: string | React.ReactNode; - text?: React.ReactNode; + description?: React.ReactNode; drawerOnMobile?: boolean; container?: HTMLElement | null; className?: string; @@ -18,6 +18,7 @@ export type DialogT = { addToTitle?: boolean; useDefaultTitle?: boolean; open?: boolean; + removeCallback?: () => void; }; export type DialogToRemove = { @@ -93,7 +94,6 @@ class Observer { this.dialogs = this.dialogs.filter((dialog) => dialog.id !== id); return; } - // Remove all dialogs for (const dialog of this.dialogs) this.publish({ id: dialog.id, remove: true, refocus }); this.dialogs = []; diff --git a/frontend/src/modules/common/error-notice.tsx b/frontend/src/modules/common/error-notice.tsx index 0dac9b30a..74f570a99 100644 --- a/frontend/src/modules/common/error-notice.tsx +++ b/frontend/src/modules/common/error-notice.tsx @@ -40,7 +40,7 @@ const ErrorNotice: React.FC = ({ error, resetErrorBoundary, is drawerOnMobile: false, className: 'sm:max-w-5xl', title: t('common:contact_us'), - text: t('common:contact_us.text'), + description: t('common:contact_us.text'), }); } window.Gleap.openConversations(); diff --git a/frontend/src/modules/common/main-footer.tsx b/frontend/src/modules/common/main-footer.tsx index 036a191f5..1982183c2 100644 --- a/frontend/src/modules/common/main-footer.tsx +++ b/frontend/src/modules/common/main-footer.tsx @@ -43,7 +43,7 @@ export const FooterLinks = ({ links = defaultFooterLinks, className = '' }: Foot drawerOnMobile: false, className: 'sm:max-w-5xl', title: t('common:contact_us'), - text: t('common:contact_us.text'), + description: t('common:contact_us.text'), }); }; diff --git a/frontend/src/modules/common/main-nav/index.tsx b/frontend/src/modules/common/main-nav/index.tsx index b4d367183..9bf176651 100644 --- a/frontend/src/modules/common/main-nav/index.tsx +++ b/frontend/src/modules/common/main-nav/index.tsx @@ -148,7 +148,7 @@ const AppNav = () => {
    {renderedItems.map((navItem: NavItem, index: number) => { const isSecondItem = index === 1; - const isActive = sheet.get(navItem.id); + const isActive = !!sheet.get(navItem.id); const listItemClass = renderedItems.length > 2 && isSecondItem diff --git a/frontend/src/modules/common/sheeter/drawer.tsx b/frontend/src/modules/common/sheeter/drawer.tsx index a43fd0c4f..39d741a52 100644 --- a/frontend/src/modules/common/sheeter/drawer.tsx +++ b/frontend/src/modules/common/sheeter/drawer.tsx @@ -1,25 +1,12 @@ import type { SheetProp } from '~/modules/common/sheeter/sheet'; import { Drawer, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle } from '~/modules/ui/drawer'; -export default function MobileSheet({ id, side = 'right', title, description, modal = true, content, className, removeSheet }: SheetProp) { +export default function MobileSheet({ sheet, removeSheet }: SheetProp) { + const { modal = true, side = 'right', description, title, className, content } = sheet; + return ( - { - if (modal) return removeSheet(); - // to prevent reopen on menu nav click - const target = e.target as HTMLElement; - if (!target) return; - // Find the button element based on its id or any child element - const button = document.getElementById(id); - // Check if the click event target is the button itself or any of its children - if (button && (button === target || button.contains(target))) return; - removeSheet(); - }} - direction={side} - className={className} - > + {title} {description} diff --git a/frontend/src/modules/common/sheeter/index.tsx b/frontend/src/modules/common/sheeter/index.tsx index 033c1a0df..97d6beab3 100644 --- a/frontend/src/modules/common/sheeter/index.tsx +++ b/frontend/src/modules/common/sheeter/index.tsx @@ -1,5 +1,5 @@ import { useNavigate } from '@tanstack/react-router'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { useBreakpoints } from '~/hooks/use-breakpoints'; import MobileSheet from '~/modules/common/sheeter/drawer'; @@ -8,15 +8,11 @@ import { type SheetAction, SheetObserver, type SheetT, sheet } from '~/modules/c import { objectKeys } from '~/utils/object'; export function Sheeter() { - const isMobile = useBreakpoints('max', 'sm'); const navigate = useNavigate(); - + const isMobile = useBreakpoints('max', 'sm'); const prevFocusedElement = useRef(null); + const [currentSheets, setCurrentSheets] = useState([]); - const handleRemoveSheet = useCallback((id: string) => { - setCurrentSheets((prevSheets) => prevSheets.filter((sheet) => sheet.id !== id)); - if (prevFocusedElement.current) setTimeout(() => prevFocusedElement.current?.focus(), 1); - }, []); const removeSheet = () => { navigate({ @@ -35,20 +31,18 @@ export function Sheeter() { }; useEffect(() => { - // To triggers sheets that opens on mount - setCurrentSheets(sheet.getAll()); - - const handleAction = (action: SheetAction & SheetT) => { - if (action.remove) handleRemoveSheet(action.id); - else { - prevFocusedElement.current = document.activeElement as HTMLElement; - setCurrentSheets((prevSheets) => { - const updatedSheets = prevSheets.filter((sheet) => sheet.id !== action.id); - return [...updatedSheets, action]; - }); + return SheetObserver.subscribe((action: SheetAction & SheetT) => { + if ('remove' in action) { + setCurrentSheets((prevSheets) => prevSheets.filter((sheet) => sheet.id !== action.id)); + if (prevFocusedElement.current) setTimeout(() => prevFocusedElement.current?.focus(), 1); + return; } - }; - return SheetObserver.subscribe(handleAction); + prevFocusedElement.current = document.activeElement as HTMLElement; + setCurrentSheets((prevSheets) => { + const updatedSheets = prevSheets.filter((sheet) => sheet.id !== action.id); + return [...updatedSheets, action]; + }); + }); }, []); if (!currentSheets.length) return null; @@ -57,22 +51,7 @@ export function Sheeter() { <> {currentSheets.map((sheet) => { const SheetComponent = isMobile ? MobileSheet : DesktopSheet; - return ( - { - removeSheet(); - sheet.removeCallback?.(); - }} - /> - ); + return ; })} ); diff --git a/frontend/src/modules/common/sheeter/sheet.tsx b/frontend/src/modules/common/sheeter/sheet.tsx index 5565080b6..a06733b2d 100644 --- a/frontend/src/modules/common/sheeter/sheet.tsx +++ b/frontend/src/modules/common/sheeter/sheet.tsx @@ -1,36 +1,33 @@ -import { X } from 'lucide-react'; -import { useTranslation } from 'react-i18next'; import StickyBox from '~/modules/common/sticky-box'; -import { Sheet, SheetClose, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '~/modules/ui/sheet'; +import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '~/modules/ui/sheet'; +import type { SheetT } from './state'; export interface SheetProp { - id: string; - content?: React.ReactNode; - title?: string | React.ReactNode; - description?: React.ReactNode; - modal?: boolean; - side?: 'bottom' | 'top' | 'right' | 'left'; - className?: string; + sheet: SheetT; removeSheet: () => void; } -export default function DesktopSheet({ id, title, description, modal = true, side = 'right', content, className, removeSheet }: SheetProp) { - const { t } = useTranslation(); +export default function DesktopSheet({ sheet, removeSheet }: SheetProp) { + const { modal = true, side = 'right', description, title, hideClose = true, className, content } = sheet; const handleClose = (state: boolean) => { - if (!state) removeSheet(); + if (!state) { + removeSheet(); + sheet.removeCallback?.(); + } }; return ( - + {title} - - - - {t('common:close')} - {description} diff --git a/frontend/src/modules/common/sheeter/state.ts b/frontend/src/modules/common/sheeter/state.ts index 33b70abdb..c26885d61 100644 --- a/frontend/src/modules/common/sheeter/state.ts +++ b/frontend/src/modules/common/sheeter/state.ts @@ -1,9 +1,10 @@ export type SheetT = { id: string; title?: string | React.ReactNode; - text?: React.ReactNode; + description?: React.ReactNode; className?: string; content?: React.ReactNode; + hideClose?: boolean; modal?: boolean; side?: 'bottom' | 'top' | 'right' | 'left'; removeCallback?: () => void; @@ -15,10 +16,8 @@ export type SheetAction = { }; class SheetsStateObserver { - // Array to store the current sheets - private sheets: SheetT[] = []; - // Array to store subscribers that will be notified of changes - private subscribers: Array<(action: SheetAction & SheetT) => void> = []; + private sheets: SheetT[] = []; // Array to store the current sheets + private subscribers: Array<(action: SheetAction & SheetT) => void> = []; // Store subscribers that will be notified of changes // Method to subscribe to changes subscribe = (subscriber: (action: SheetAction & SheetT) => void) => { @@ -33,11 +32,9 @@ class SheetsStateObserver { for (const sub of this.subscribers) sub(action); }; - // Retrieve a sheet by its ID - get = (id: string) => this.sheets.find((sheet) => sheet.id === id); + get = (id: string) => this.sheets.find((sheet) => sheet.id === id); // Retrieve a sheet by its ID - // Retrieve a all sheets - getAll = () => this.sheets; + getAll = () => this.sheets; // Retrieve a all sheets // Add or update a sheet and notify subscribers set = (sheet: SheetT) => { @@ -48,11 +45,9 @@ class SheetsStateObserver { // Remove a sheet by its ID or clear all sheets and notify subscribers remove = (id?: string) => { if (id) { - // Remove a specific sheet by ID this.sheets = this.sheets.filter((sheet) => sheet.id !== id); this.notifySubscribers({ id, remove: true }); } else { - // Remove all sheets for (const sheet of this.sheets) this.notifySubscribers({ id: sheet.id, remove: true }); this.sheets = []; } @@ -78,12 +73,10 @@ class SheetsStateObserver { export const SheetObserver = new SheetsStateObserver(); -// TODO this does not have type safety? -// Also, it seems the sheet responds a bit slow when opening and closing programmatically -export const sheet = Object.assign({ - create: SheetObserver.create, - remove: SheetObserver.remove, - update: SheetObserver.update, - get: SheetObserver.get, - getAll: SheetObserver.getAll, -}); +export const sheet = { + create: SheetObserver.create.bind(SheetObserver), + remove: SheetObserver.remove.bind(SheetObserver), + update: SheetObserver.update.bind(SheetObserver), + get: SheetObserver.get.bind(SheetObserver), + getAll: SheetObserver.getAll.bind(SheetObserver), +}; diff --git a/frontend/src/modules/home/onboarding/footer.tsx b/frontend/src/modules/home/onboarding/footer.tsx index da77e4651..ad113f4a0 100644 --- a/frontend/src/modules/home/onboarding/footer.tsx +++ b/frontend/src/modules/home/onboarding/footer.tsx @@ -28,7 +28,7 @@ const StepperFooter = ({ dialog(, { className: 'md:max-w-xl', title: `${t('common:skip')} ${t('common:create_resource', { resource: t('common:organization') }).toLowerCase()}`, - text: t('common:skip_org_creation.text'), + description: t('common:skip_org_creation.text'), id: 'skip_org_creation', }); return; diff --git a/frontend/src/modules/marketing/about/pricing.tsx b/frontend/src/modules/marketing/about/pricing.tsx index b0583d32c..c893552ac 100644 --- a/frontend/src/modules/marketing/about/pricing.tsx +++ b/frontend/src/modules/marketing/about/pricing.tsx @@ -21,7 +21,7 @@ const Pricing = () => { drawerOnMobile: false, className: 'sm:max-w-5xl', title: t('common:contact_us'), - text: t('common:contact_us.text'), + description: t('common:contact_us.text'), }); } if (action === 'sign_in') { @@ -33,7 +33,7 @@ const Pricing = () => { drawerOnMobile: true, className: 'sm:max-w-2xl', title: t('common:waitlist_request'), - text: t('common:waitlist_request.text', { appName: config.name }), + description: t('common:waitlist_request.text', { appName: config.name }), }); } }; diff --git a/frontend/src/modules/organizations/members-table/index.tsx b/frontend/src/modules/organizations/members-table/index.tsx index fcd7badd6..b7d1b5fb5 100644 --- a/frontend/src/modules/organizations/members-table/index.tsx +++ b/frontend/src/modules/organizations/members-table/index.tsx @@ -189,7 +189,7 @@ const MembersTable = ({ entity, isSheet = false }: MembersTableProps) => { container: containerRef.current, containerBackdrop: false, title: t('common:invite'), - text: `${t('common:invite_users.text')}`, + description: `${t('common:invite_users.text')}`, }); }; @@ -209,7 +209,7 @@ const MembersTable = ({ entity, isSheet = false }: MembersTableProps) => { { className: 'max-w-xl', title: t('common:remove_resource', { resource: t('member').toLowerCase() }), - text: ( + description: ( { drawerOnMobile: false, className: 'max-w-xl', title: t('common:delete'), - text: t('common:confirm.delete_resources', { resources: t('common:organizations').toLowerCase() }), + description: t('common:confirm.delete_resources', { resources: t('common:organizations').toLowerCase() }), }, ); }; @@ -150,7 +150,7 @@ const OrganizationsTable = () => { { className: 'max-w-full lg:max-w-4xl', title: t('common:newsletter'), - text: t('common:newsletter.text'), + description: t('common:newsletter.text'), id: 'newsletter-form', }, ); diff --git a/frontend/src/modules/organizations/update-organization-form.tsx b/frontend/src/modules/organizations/update-organization-form.tsx index 4eb896052..946713e50 100644 --- a/frontend/src/modules/organizations/update-organization-form.tsx +++ b/frontend/src/modules/organizations/update-organization-form.tsx @@ -7,7 +7,7 @@ import { type UpdateOrganizationParams, updateOrganization } from '~/api/organiz import type { Organization } from '~/types/common'; import { config } from 'config'; -import { useEffect } from 'react'; +import { isValidElement, useEffect } from 'react'; import { type UseFormProps, useWatch } from 'react-hook-form'; import { toast } from 'sonner'; import { useBeforeUnload } from '~/hooks/use-before-unload'; @@ -113,12 +113,16 @@ const UpdateOrganizationForm = ({ organization, callback, sheet: isSheet }: Prop useEffect(() => { if (form.unsavedChanges) { const targetSheet = sheet.get('update-organization'); - if (targetSheet && targetSheet.title?.type?.name !== 'UnsavedBadge') { - sheet.update('update-organization', { - title: , - }); - } - return; + + if (!targetSheet || !isValidElement(targetSheet.title)) return; + // Check if the title's type is a function (React component) and not a string + const { type: tittleType } = targetSheet.title; + + if (typeof tittleType !== 'function' || tittleType.name === 'UnsavedBadge') return; + + sheet.update('update-organization', { + title: , + }); } }, [form.unsavedChanges]); diff --git a/frontend/src/modules/ui/sheet.tsx b/frontend/src/modules/ui/sheet.tsx index df68151ae..a884113dc 100644 --- a/frontend/src/modules/ui/sheet.tsx +++ b/frontend/src/modules/ui/sheet.tsx @@ -1,6 +1,7 @@ import * as SheetPrimitive from '@radix-ui/react-dialog'; import * as VisuallyHidden from '@radix-ui/react-visually-hidden'; import { type VariantProps, cva } from 'class-variance-authority'; +import { X } from 'lucide-react'; import * as React from 'react'; import { cn } from '~/utils/cn'; @@ -48,15 +49,22 @@ export const sheetVariants = cva( interface SheetContentProps extends React.ComponentPropsWithoutRef, VariantProps { onClick?: () => void; // Adding onClick prop + hideClose?: boolean; } const SheetContent = React.forwardRef, SheetContentProps>( - ({ side = 'right', className, children, onClick, ...props }, ref) => ( + ({ side = 'right', className, children, hideClose = true, onClick, ...props }, ref) => ( <> {children} + {!hideClose && ( + + + Close + + )} ), diff --git a/frontend/src/modules/users/settings-page.tsx b/frontend/src/modules/users/settings-page.tsx index c30239166..bcba79ef5 100644 --- a/frontend/src/modules/users/settings-page.tsx +++ b/frontend/src/modules/users/settings-page.tsx @@ -74,7 +74,7 @@ const UserSettingsPage = () => { { className: 'md:max-w-xl', title: t('common:delete_account'), - text: t('common:confirm.delete_account', { email: user.email }), + description: t('common:confirm.delete_account', { email: user.email }), }, ); }; diff --git a/frontend/src/modules/users/update-user-form.tsx b/frontend/src/modules/users/update-user-form.tsx index 9bd5a9649..2fcfbee4c 100644 --- a/frontend/src/modules/users/update-user-form.tsx +++ b/frontend/src/modules/users/update-user-form.tsx @@ -15,6 +15,7 @@ import { Button } from '~/modules/ui/button'; import { Checkbox } from '~/modules/ui/checkbox'; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '~/modules/ui/form'; +import { isValidElement } from 'react'; import type { UseFormProps } from 'react-hook-form'; import { toast } from 'sonner'; import { useFormWithDraft } from '~/hooks/use-draft-form'; @@ -114,12 +115,15 @@ const UpdateUserForm = ({ user, callback, sheet: isSheet, hiddenFields, children useEffect(() => { if (form.unsavedChanges) { const targetSheet = sheet.get('update-user'); - if (targetSheet && targetSheet.title?.type?.name !== 'UnsavedBadge') { - sheet.update('update-user', { - title: , - }); - } - return; + + if (!targetSheet || !isValidElement(targetSheet.title)) return; + // Check if the title's type is a function (React component) and not a string + const { type: tittleType } = targetSheet.title; + + if (typeof tittleType !== 'function' || tittleType.name === 'UnsavedBadge') return; + sheet.update('update-user', { + title: , + }); } }, [form.unsavedChanges]); diff --git a/frontend/src/modules/users/users-table/index.tsx b/frontend/src/modules/users/users-table/index.tsx index fcce208fe..42f365f54 100644 --- a/frontend/src/modules/users/users-table/index.tsx +++ b/frontend/src/modules/users/users-table/index.tsx @@ -140,7 +140,7 @@ const UsersTable = () => { container: containerRef.current, containerBackdrop: false, title: t('common:invite'), - text: `${t('common:invite_users.text')}`, + description: `${t('common:invite_users.text')}`, }); }; @@ -158,7 +158,7 @@ const UsersTable = () => { drawerOnMobile: false, className: 'max-w-xl', title: t('common:delete'), - text: t('common:confirm.delete_resource', { + description: t('common:confirm.delete_resource', { name: selectedUsers.map((u) => u.email).join(', '), resource: selectedUsers.length > 1 ? t('common:users').toLowerCase() : t('common:user').toLowerCase(), }), From 818ae08989a29f962aaeeef6385a24203383f3ef Mon Sep 17 00:00:00 2001 From: David Hordiienko Date: Mon, 7 Oct 2024 16:59:07 +0300 Subject: [PATCH 02/28] implement: float nav --- .../bar-nav-button.tsx} | 2 +- .../bar-nav-loader.tsx} | 0 .../modules/common/main-nav/bar-nav/index.tsx | 86 +++++++++++++++++ .../main-nav/float-nav/button-container.tsx | 28 ++++++ .../common/main-nav/float-nav/index.tsx | 52 +++++++++++ .../src/modules/common/main-nav/index.tsx | 93 +++---------------- 6 files changed, 179 insertions(+), 82 deletions(-) rename frontend/src/modules/common/main-nav/{main-nav-button.tsx => bar-nav/bar-nav-button.tsx} (95%) rename frontend/src/modules/common/main-nav/{main-nav-loader.tsx => bar-nav/bar-nav-loader.tsx} (100%) create mode 100644 frontend/src/modules/common/main-nav/bar-nav/index.tsx create mode 100644 frontend/src/modules/common/main-nav/float-nav/button-container.tsx create mode 100644 frontend/src/modules/common/main-nav/float-nav/index.tsx diff --git a/frontend/src/modules/common/main-nav/main-nav-button.tsx b/frontend/src/modules/common/main-nav/bar-nav/bar-nav-button.tsx similarity index 95% rename from frontend/src/modules/common/main-nav/main-nav-button.tsx rename to frontend/src/modules/common/main-nav/bar-nav/bar-nav-button.tsx index 2755738de..073319082 100644 --- a/frontend/src/modules/common/main-nav/main-nav-button.tsx +++ b/frontend/src/modules/common/main-nav/bar-nav/bar-nav-button.tsx @@ -7,7 +7,7 @@ import { useUserStore } from '~/store/user'; import { useTranslation } from 'react-i18next'; import type { NavItem } from '~/modules/common/main-nav'; -import MainNavLoader from '~/modules/common/main-nav/main-nav-loader'; +import MainNavLoader from '~/modules/common/main-nav/bar-nav/bar-nav-loader'; import { TooltipButton } from '~/modules/common/tooltip-button'; import { cn } from '~/utils/cn'; diff --git a/frontend/src/modules/common/main-nav/main-nav-loader.tsx b/frontend/src/modules/common/main-nav/bar-nav/bar-nav-loader.tsx similarity index 100% rename from frontend/src/modules/common/main-nav/main-nav-loader.tsx rename to frontend/src/modules/common/main-nav/bar-nav/bar-nav-loader.tsx diff --git a/frontend/src/modules/common/main-nav/bar-nav/index.tsx b/frontend/src/modules/common/main-nav/bar-nav/index.tsx new file mode 100644 index 000000000..353409c54 --- /dev/null +++ b/frontend/src/modules/common/main-nav/bar-nav/index.tsx @@ -0,0 +1,86 @@ +import { useNavigate } from '@tanstack/react-router'; +import { config } from 'config'; +import { UserX } from 'lucide-react'; +import { Fragment, Suspense, lazy, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { toast } from 'sonner'; +import { impersonationStop } from '~/api/auth'; +import useBodyClass from '~/hooks/use-body-class'; +import useMounted from '~/hooks/use-mounted'; +import type { NavItem } from '~/modules/common/main-nav'; +import { NavButton } from '~/modules/common/main-nav/bar-nav/bar-nav-button'; +import { getAndSetMe, getAndSetMenu } from '~/modules/users/helpers'; +import type { NavItemId } from '~/nav-config'; +import { useNavigationStore } from '~/store/navigation'; +import { useThemeStore } from '~/store/theme'; +import { useUserStore } from '~/store/user'; +import { cn } from '~/utils/cn'; + +const DebugToolbars = config.mode === 'development' ? lazy(() => import('~/modules/common/debug-toolbars')) : () => null; + +const BarNav = ({ items, onClick }: { items: NavItem[]; onClick: (id: NavItemId, index: number) => void }) => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const { hasStarted } = useMounted(); + + const { user } = useUserStore(); + const { theme } = useThemeStore(); + const { focusView, keepMenuOpen, navSheetOpen } = useNavigationStore(); + + const currentSession = useMemo(() => user?.sessions.find((s) => s.isCurrent), [user]); + + // Keep menu open + useBodyClass({ 'keep-nav-open': keepMenuOpen, 'nav-open': !!navSheetOpen }); + + const stopImpersonation = async () => { + await impersonationStop(); + await Promise.all([getAndSetMe(), getAndSetMenu()]); + navigate({ to: config.defaultRedirectPath, replace: true }); + toast.success(t('common:success.stopped_impersonation')); + }; + + const navBackground = theme !== 'none' ? 'bg-primary' : 'bg-primary-foreground'; + return ( + + ); +}; + +export default BarNav; diff --git a/frontend/src/modules/common/main-nav/float-nav/button-container.tsx b/frontend/src/modules/common/main-nav/float-nav/button-container.tsx new file mode 100644 index 000000000..57d2da27a --- /dev/null +++ b/frontend/src/modules/common/main-nav/float-nav/button-container.tsx @@ -0,0 +1,28 @@ +import type { LucideProps } from 'lucide-react'; +import type React from 'react'; +import { Button } from '~/modules/ui/button'; + +interface MobileNavButtonProps { + Icon: React.ElementType; + onClick: () => void; + className?: string; + direction?: 'left' | 'right'; +} + +const MobileNavButton: React.FC = ({ Icon, onClick, className, direction = 'right' }) => { + const positionClasses = `${direction}-3`; + + return ( + + ); +}; + +export default MobileNavButton; diff --git a/frontend/src/modules/common/main-nav/float-nav/index.tsx b/frontend/src/modules/common/main-nav/float-nav/index.tsx new file mode 100644 index 000000000..7497e8508 --- /dev/null +++ b/frontend/src/modules/common/main-nav/float-nav/index.tsx @@ -0,0 +1,52 @@ +import { useEffect, useState } from 'react'; +import type { NavItem } from '~/modules/common/main-nav'; +import type { NavItemId } from '~/nav-config'; +import MobileNavButton from './button-container'; + +const FloatNav = ({ items, onClick }: { items: NavItem[]; onClick: (id: NavItemId, index: number) => void }) => { + const [showButtons, setShowButtons] = useState(true); + const [lastScrollY, setLastScrollY] = useState(0); + + useEffect(() => { + const handleScroll = () => { + const currentScrollY = window.scrollY; + // User is scrolling down, hide buttons. Up, show buttons + if (currentScrollY > lastScrollY) setShowButtons(false); + else setShowButtons(true); + + // Update last scroll position + setLastScrollY(currentScrollY); + }; + + window.addEventListener('scroll', handleScroll); + return () => window.removeEventListener('scroll', handleScroll); + }, [lastScrollY]); + + useEffect(() => { + const mainAppRoot = document.getElementById('main-app-root'); + + if (mainAppRoot) mainAppRoot.style.height = 'auto'; + return () => { + if (mainAppRoot) mainAppRoot.style.height = ''; + }; + }, []); + + return ( + + ); +}; + +export default FloatNav; diff --git a/frontend/src/modules/common/main-nav/index.tsx b/frontend/src/modules/common/main-nav/index.tsx index 9bf176651..1b638b17c 100644 --- a/frontend/src/modules/common/main-nav/index.tsx +++ b/frontend/src/modules/common/main-nav/index.tsx @@ -1,26 +1,15 @@ import { useNavigate } from '@tanstack/react-router'; -import { config } from 'config'; -import { type LucideProps, UserX } from 'lucide-react'; -import { Fragment, Suspense, lazy, useEffect, useMemo } from 'react'; -import { useThemeStore } from '~/store/theme'; - +import type { LucideProps } from 'lucide-react'; +import { useEffect, useMemo } from 'react'; import { useBreakpoints } from '~/hooks/use-breakpoints'; -import { dialog } from '~/modules/common/dialoger/state'; -import { useNavigationStore } from '~/store/navigation'; -import { cn } from '~/utils/cn'; - -import { useTranslation } from 'react-i18next'; -import { toast } from 'sonner'; -import { impersonationStop } from '~/api/auth'; -import useBodyClass from '~/hooks/use-body-class'; import { useHotkeys } from '~/hooks/use-hot-keys'; -import useMounted from '~/hooks/use-mounted'; import router from '~/lib/router'; -import { NavButton } from '~/modules/common/main-nav/main-nav-button'; +import { dialog } from '~/modules/common/dialoger/state'; +import BarNav from '~/modules/common/main-nav/bar-nav'; +import FloatNav from '~/modules/common/main-nav/float-nav'; import { sheet } from '~/modules/common/sheeter/state'; -import { getAndSetMe, getAndSetMenu } from '~/modules/users/helpers'; import { type NavItemId, baseNavItems, navItems } from '~/nav-config'; -import { useUserStore } from '~/store/user'; +import { useNavigationStore } from '~/store/navigation'; export type NavItem = { id: NavItemId; @@ -31,19 +20,11 @@ export type NavItem = { mirrorOnMobile?: boolean; }; -const DebugToolbars = config.mode === 'development' ? lazy(() => import('~/modules/common/debug-toolbars')) : () => null; - -const AppNav = () => { +const MainNav = () => { const navigate = useNavigate(); - const { t } = useTranslation(); - const { hasStarted } = useMounted(); const isMobile = useBreakpoints('max', 'sm'); - const { setLoading, setFocusView, focusView, keepMenuOpen, navSheetOpen, setNavSheetOpen } = useNavigationStore(); - const { theme } = useThemeStore(); - const { user } = useUserStore(); - - const currentSession = useMemo(() => user?.sessions.find((s) => s.isCurrent), [user]); + const { setLoading, setFocusView, navSheetOpen, setNavSheetOpen } = useNavigationStore(); const showedNavButtons = useMemo(() => { const desktop = router.state.matches.flatMap((el) => el.staticData.showedDesktopNavButtons || []); @@ -56,17 +37,7 @@ const AppNav = () => { return navItems.filter(({ id }) => itemsIds.includes(id)); }, [showedNavButtons]); - // Keep menu open - useBodyClass({ 'keep-nav-open': keepMenuOpen, 'nav-open': !!navSheetOpen }); - - const stopImpersonation = async () => { - await impersonationStop(); - await Promise.all([getAndSetMe(), getAndSetMenu()]); - navigate({ to: config.defaultRedirectPath, replace: true }); - toast.success(t('common:success.stopped_impersonation')); - }; - - const navBackground = theme !== 'none' ? 'bg-primary' : 'bg-primary-foreground'; + const showFloatNav = renderedItems.length > 0 && renderedItems.length <= 2; const navButtonClick = (navItem: NavItem) => { // If its a have dialog, open it @@ -135,48 +106,8 @@ const AppNav = () => { }); }, []); - return ( - - ); + const NavComponent = showFloatNav ? FloatNav : BarNav; + return ; }; -export default AppNav; +export default MainNav; From ec81f38601839b543114dba8e8c5f77f8ad7f948 Mon Sep 17 00:00:00 2001 From: David Hordiienko Date: Tue, 8 Oct 2024 11:21:31 +0300 Subject: [PATCH 03/28] refactor: sheeter & dialoger --- .../src/modules/common/data-table/util.tsx | 30 ++++++++++++- .../src/modules/common/dialoger/dialog.tsx | 20 ++++++--- .../src/modules/common/dialoger/drawer.tsx | 11 ++++- .../src/modules/common/dialoger/index.tsx | 39 +++------------- frontend/src/modules/common/dialoger/state.ts | 19 +++----- .../modules/common/main-nav/bar-nav/index.tsx | 5 +-- .../common/main-nav/float-nav/index.tsx | 12 +---- .../src/modules/common/main-nav/index.tsx | 20 +++++---- .../src/modules/common/sheeter/drawer.tsx | 17 +++++-- frontend/src/modules/common/sheeter/index.tsx | 45 +++++-------------- frontend/src/modules/common/sheeter/sheet.tsx | 24 +++++----- frontend/src/modules/common/sheeter/state.ts | 17 ++++--- .../organizations/members-table/index.tsx | 13 +----- .../system/organizations-newsletter-form.tsx | 4 +- frontend/src/modules/ui/sheet.tsx | 5 ++- .../src/modules/users/users-table/columns.tsx | 11 +---- .../src/modules/users/users-table/index.tsx | 6 ++- 17 files changed, 143 insertions(+), 155 deletions(-) diff --git a/frontend/src/modules/common/data-table/util.tsx b/frontend/src/modules/common/data-table/util.tsx index b93dd6f79..7e8b738ab 100644 --- a/frontend/src/modules/common/data-table/util.tsx +++ b/frontend/src/modules/common/data-table/util.tsx @@ -1,10 +1,23 @@ +import type { NavigateFn } from '@tanstack/react-router'; import { Suspense, lazy } from 'react'; import { sheet } from '~/modules/common/sheeter/state'; import type { User } from '~/types/common'; +import { objectKeys } from '~/utils/object'; const UserProfilePage = lazy(() => import('~/modules/users/profile-page')); -export const openUserPreviewSheet = (user: Omit) => { +export const openUserPreviewSheet = (user: Omit, navigate: NavigateFn, addSearch = false) => { + if (addSearch) { + navigate({ + to: '.', + replace: true, + resetScroll: false, + search: (prev) => ({ + ...prev, + ...{ userIdPreview: user.id }, + }), + }); + } sheet.create( @@ -12,6 +25,21 @@ export const openUserPreviewSheet = (user: Omit) => { { className: 'max-w-full lg:max-w-4xl p-0', id: `user-preview-${user.id}`, + removeCallback: () => { + navigate({ + to: '.', + replace: true, + resetScroll: false, + search: (prev) => { + const newSearch = { ...prev }; + for (const key of objectKeys(newSearch)) { + if (key.includes('Preview')) delete newSearch[key]; + } + return newSearch; + }, + }); + sheet.remove(`user-preview-${user.id}`); + }, }, ); }; diff --git a/frontend/src/modules/common/dialoger/dialog.tsx b/frontend/src/modules/common/dialoger/dialog.tsx index d907d5eee..39920fcbb 100644 --- a/frontend/src/modules/common/dialoger/dialog.tsx +++ b/frontend/src/modules/common/dialoger/dialog.tsx @@ -1,19 +1,29 @@ -import type { DialogT } from '~/modules/common/dialoger/state'; +import { type DialogT, dialog as dialogState } from '~/modules/common/dialoger/state'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '~/modules/ui/dialog'; export interface DialogProp { dialog: DialogT; - onOpenChange: () => void; + removeDialog: (dialog: DialogT) => void; } -export default function StandardDialog({ dialog, onOpenChange }: DialogProp) { - const { id, content, container, description, title, className, containerBackdrop, autoFocus, hideClose } = dialog; +export default function StandardDialog({ dialog, removeDialog }: DialogProp) { + const { id, content, container, open, description, title, className, containerBackdrop, autoFocus, hideClose } = dialog; + + const closeDialog = () => { + removeDialog(dialog); + dialog.removeCallback?.(); + }; + const onOpenChange = (open: boolean) => { + dialogState.update(dialog.id, { open }); + if (!open) closeDialog(); + }; return ( - + {container && containerBackdrop && (
    )} { if (container && !containerBackdrop) e.preventDefault(); }} diff --git a/frontend/src/modules/common/dialoger/drawer.tsx b/frontend/src/modules/common/dialoger/drawer.tsx index 94445f1b0..e2cef1bbb 100644 --- a/frontend/src/modules/common/dialoger/drawer.tsx +++ b/frontend/src/modules/common/dialoger/drawer.tsx @@ -1,9 +1,18 @@ import type { DialogProp } from '~/modules/common/dialoger/dialog'; +import { dialog as dialogState } from '~/modules/common/dialoger/state'; import { Drawer, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle } from '~/modules/ui/drawer'; -export default function DrawerDialog({ dialog, onOpenChange }: DialogProp) { +export default function DrawerDialog({ dialog, removeDialog }: DialogProp) { const { id, content, open, description, title, className } = dialog; + const onOpenChange = (open: boolean) => { + dialogState.update(dialog.id, { open }); + if (!open) { + removeDialog(dialog); + dialog.removeCallback?.(); + } + }; + return ( diff --git a/frontend/src/modules/common/dialoger/index.tsx b/frontend/src/modules/common/dialoger/index.tsx index 9b235b1ee..9eb471a4f 100644 --- a/frontend/src/modules/common/dialoger/index.tsx +++ b/frontend/src/modules/common/dialoger/index.tsx @@ -10,19 +10,7 @@ export function Dialoger() { const isMobile = useBreakpoints('max', 'sm'); const prevFocusedElement = useRef(null); - const updateDialog = (dialog: DialogT, open: boolean) => { - DialogState.update(dialog.id, { open }); - if (!open) { - removeDialog(dialog); - dialog.removeCallback?.(); - } - }; - const onOpenChange = (dialog: DialogT) => (open: boolean) => { - updateDialog(dialog, open); - }; - const removeDialog = useCallback((dialog: DialogT | DialogToRemove) => { - DialogState.update(dialog.id, { open: false }); setDialogs((dialogs) => dialogs.filter(({ id }) => id !== dialog.id)); if (dialog.refocus && prevFocusedElement.current) { // Timeout is needed to prevent focus from being stolen by the dialog that was just removed @@ -35,26 +23,13 @@ export function Dialoger() { useEffect(() => { return DialogState.subscribe((dialog) => { - if ('remove' in dialog) { - removeDialog(dialog); - return; - } - if ('reset' in dialog) { - setUpdatedDialogs((updatedDialogs) => updatedDialogs.filter(({ id }) => id !== dialog.id)); - return; - } - prevFocusedElement.current = (document.activeElement || document.body) as HTMLElement; - setUpdatedDialogs((updatedDialogs) => { - const existingDialog = updatedDialogs.find(({ id }) => id === dialog.id); - if (existingDialog) return updatedDialogs.map((d) => (d.id === dialog.id ? dialog : d)); + if ('remove' in dialog) return removeDialog(dialog); - return [...updatedDialogs, dialog]; - }); - setDialogs((dialogs) => { - const existingDialog = dialogs.find(({ id }) => id === dialog.id); - if (existingDialog) return dialogs; - return [...dialogs, dialog]; - }); + if ('reset' in dialog) return setUpdatedDialogs((updatedDialogs) => updatedDialogs.filter(({ id }) => id !== dialog.id)); + + prevFocusedElement.current = (document.activeElement || document.body) as HTMLElement; + setUpdatedDialogs((updatedDialogs) => [...updatedDialogs.filter((d) => d.id !== dialog.id), dialog]); + setDialogs((dialogs) => [...dialogs.filter((d) => d.id !== dialog.id), dialog]); }); }, []); @@ -63,6 +38,6 @@ export function Dialoger() { return dialogs.map((dialog) => { const existingDialog = updatedDialogs.find(({ id }) => id === dialog.id); const DialogComponent = !isMobile || !dialog.drawerOnMobile ? StandardDialog : DrawerDialog; - return onOpenChange(dialog)} />; + return ; }); } diff --git a/frontend/src/modules/common/dialoger/state.ts b/frontend/src/modules/common/dialoger/state.ts index cc2ba3b0d..45f90d1cd 100644 --- a/frontend/src/modules/common/dialoger/state.ts +++ b/frontend/src/modules/common/dialoger/state.ts @@ -61,9 +61,7 @@ class Observer { }; publish = (data: DialogT | DialogToRemove | DialogToReset) => { - for (const subscriber of this.subscribers) { - subscriber(data); - } + for (const subscriber of this.subscribers) subscriber(data); }; set = (data: DialogT) => { @@ -71,22 +69,15 @@ class Observer { const existingDialogIndex = this.dialogs.findIndex((dialog) => dialog.id === data.id); // If it exists, replace it, otherwise add it - if (existingDialogIndex > -1) { - this.dialogs[existingDialogIndex] = data; - } else { - this.dialogs = [...this.dialogs, data]; - } + if (existingDialogIndex > -1) this.dialogs[existingDialogIndex] = data; + else this.dialogs = [...this.dialogs, data]; this.publish(data); }; - get = (id: number | string) => { - return this.dialogs.find((dialog) => dialog.id === id); - }; + get = (id: number | string) => this.dialogs.find((dialog) => dialog.id === id); - haveOpenDialogs = () => { - return this.dialogs.some((d) => isDialog(d) && d.open); - }; + haveOpenDialogs = () => this.dialogs.some((d) => isDialog(d) && d.open); remove = (refocus = true, id?: number | string) => { if (id) { diff --git a/frontend/src/modules/common/main-nav/bar-nav/index.tsx b/frontend/src/modules/common/main-nav/bar-nav/index.tsx index 353409c54..acfa15f7b 100644 --- a/frontend/src/modules/common/main-nav/bar-nav/index.tsx +++ b/frontend/src/modules/common/main-nav/bar-nav/index.tsx @@ -10,7 +10,6 @@ import useMounted from '~/hooks/use-mounted'; import type { NavItem } from '~/modules/common/main-nav'; import { NavButton } from '~/modules/common/main-nav/bar-nav/bar-nav-button'; import { getAndSetMe, getAndSetMenu } from '~/modules/users/helpers'; -import type { NavItemId } from '~/nav-config'; import { useNavigationStore } from '~/store/navigation'; import { useThemeStore } from '~/store/theme'; import { useUserStore } from '~/store/user'; @@ -18,7 +17,7 @@ import { cn } from '~/utils/cn'; const DebugToolbars = config.mode === 'development' ? lazy(() => import('~/modules/common/debug-toolbars')) : () => null; -const BarNav = ({ items, onClick }: { items: NavItem[]; onClick: (id: NavItemId, index: number) => void }) => { +const BarNav = ({ items, onClick }: { items: NavItem[]; onClick: (index: number) => void }) => { const { t } = useTranslation(); const navigate = useNavigate(); const { hasStarted } = useMounted(); @@ -64,7 +63,7 @@ const BarNav = ({ items, onClick }: { items: NavItem[]; onClick: (id: NavItemId, {isSecondItem &&
    }
  • - onClick(navItem.id, index)} /> + onClick(index)} />
  • diff --git a/frontend/src/modules/common/main-nav/float-nav/index.tsx b/frontend/src/modules/common/main-nav/float-nav/index.tsx index 7497e8508..80c11abaf 100644 --- a/frontend/src/modules/common/main-nav/float-nav/index.tsx +++ b/frontend/src/modules/common/main-nav/float-nav/index.tsx @@ -1,9 +1,8 @@ import { useEffect, useState } from 'react'; import type { NavItem } from '~/modules/common/main-nav'; -import type { NavItemId } from '~/nav-config'; import MobileNavButton from './button-container'; -const FloatNav = ({ items, onClick }: { items: NavItem[]; onClick: (id: NavItemId, index: number) => void }) => { +const FloatNav = ({ items, onClick }: { items: NavItem[]; onClick: (index: number) => void }) => { const [showButtons, setShowButtons] = useState(true); const [lastScrollY, setLastScrollY] = useState(0); @@ -36,14 +35,7 @@ const FloatNav = ({ items, onClick }: { items: NavItem[]; onClick: (id: NavItemI {showButtons && items.map((navItem: NavItem, idx: number) => { const firstButton = items.length > 1 && idx === 0; - return ( - onClick(navItem.id, idx)} - direction={firstButton ? 'left' : 'right'} - /> - ); + return onClick(idx)} direction={firstButton ? 'left' : 'right'} />; })} ); diff --git a/frontend/src/modules/common/main-nav/index.tsx b/frontend/src/modules/common/main-nav/index.tsx index 1b638b17c..dd869dee8 100644 --- a/frontend/src/modules/common/main-nav/index.tsx +++ b/frontend/src/modules/common/main-nav/index.tsx @@ -61,7 +61,7 @@ const MainNav = () => { // Create a sheet sheet.create(navItem.sheet, { - id: `${navItem.id}-nav`, + id: 'nav-sheet', side: sheetSide, modal: isMobile, className: 'fixed sm:z-[80] p-0 sm:inset-0 xs:max-w-80 sm:left-16', @@ -71,11 +71,13 @@ const MainNav = () => { }); }; - const clickNavItem = (id: NavItemId, index: number) => { + const clickNavItem = (index: number) => { // If the nav item is already open, close it - if (id === navSheetOpen) { - sheet.remove(); - return setNavSheetOpen(null); + const id = renderedItems[index].id; + if (id === navSheetOpen && sheet.get('nav-sheet')?.open) { + setNavSheetOpen(null); + sheet.update('nav-sheet', { open: false }); + return; } if (dialog.haveOpenDialogs()) return; @@ -84,10 +86,10 @@ const MainNav = () => { }; useHotkeys([ - ['Shift + A', () => clickNavItem('account', 3)], - ['Shift + F', () => clickNavItem('search', 2)], - ['Shift + H', () => clickNavItem('home', 1)], - ['Shift + M', () => clickNavItem('menu', 0)], + ['Shift + A', () => clickNavItem(3)], + ['Shift + F', () => clickNavItem(2)], + ['Shift + H', () => clickNavItem(1)], + ['Shift + M', () => clickNavItem(0)], ]); useEffect(() => { diff --git a/frontend/src/modules/common/sheeter/drawer.tsx b/frontend/src/modules/common/sheeter/drawer.tsx index 39d741a52..976a2a560 100644 --- a/frontend/src/modules/common/sheeter/drawer.tsx +++ b/frontend/src/modules/common/sheeter/drawer.tsx @@ -1,12 +1,23 @@ import type { SheetProp } from '~/modules/common/sheeter/sheet'; import { Drawer, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle } from '~/modules/ui/drawer'; +import { sheet as sheetState } from './state'; export default function MobileSheet({ sheet, removeSheet }: SheetProp) { - const { modal = true, side = 'right', description, title, className, content } = sheet; + const { modal = true, side = 'right', description, title, className, content, open } = sheet; + + const closeSheet = () => { + removeSheet(sheet); + sheet.removeCallback?.(); + }; + + const onOpenChange = (open: boolean) => { + sheetState.update(sheet.id, { open }); + if (!open) closeSheet(); + }; return ( - - + + {title} {description} diff --git a/frontend/src/modules/common/sheeter/index.tsx b/frontend/src/modules/common/sheeter/index.tsx index 97d6beab3..9a0aa4262 100644 --- a/frontend/src/modules/common/sheeter/index.tsx +++ b/frontend/src/modules/common/sheeter/index.tsx @@ -1,55 +1,34 @@ -import { useNavigate } from '@tanstack/react-router'; -import { useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { useBreakpoints } from '~/hooks/use-breakpoints'; import MobileSheet from '~/modules/common/sheeter/drawer'; import DesktopSheet from '~/modules/common/sheeter/sheet'; -import { type SheetAction, SheetObserver, type SheetT, sheet } from '~/modules/common/sheeter/state'; -import { objectKeys } from '~/utils/object'; +import { type SheetAction, SheetObserver, type SheetT } from '~/modules/common/sheeter/state'; export function Sheeter() { - const navigate = useNavigate(); const isMobile = useBreakpoints('max', 'sm'); const prevFocusedElement = useRef(null); - const [currentSheets, setCurrentSheets] = useState([]); + const [sheets, setSheets] = useState([]); - const removeSheet = () => { - navigate({ - to: '.', - replace: true, - resetScroll: false, - search: (prev) => { - const newSearch = { ...prev }; - for (const key of objectKeys(newSearch)) { - if (key.includes('Preview')) delete newSearch[key]; - } - return newSearch; - }, - }); - sheet.remove(); - }; + const removeSheet = useCallback((sheet: SheetT) => { + setSheets((currentSheets) => currentSheets.filter(({ id }) => id !== sheet.id)); + if (prevFocusedElement.current) setTimeout(() => prevFocusedElement.current?.focus(), 1); + }, []); useEffect(() => { return SheetObserver.subscribe((action: SheetAction & SheetT) => { - if ('remove' in action) { - setCurrentSheets((prevSheets) => prevSheets.filter((sheet) => sheet.id !== action.id)); - if (prevFocusedElement.current) setTimeout(() => prevFocusedElement.current?.focus(), 1); - return; - } - prevFocusedElement.current = document.activeElement as HTMLElement; - setCurrentSheets((prevSheets) => { - const updatedSheets = prevSheets.filter((sheet) => sheet.id !== action.id); - return [...updatedSheets, action]; - }); + if ('remove' in action) removeSheet(action); + prevFocusedElement.current = (document.activeElement || document.body) as HTMLElement; + setSheets((prevSheets) => [...prevSheets.filter((sheet) => sheet.id !== action.id), action]); }); }, []); - if (!currentSheets.length) return null; + if (!sheets.length) return null; return ( <> - {currentSheets.map((sheet) => { + {sheets.map((sheet) => { const SheetComponent = isMobile ? MobileSheet : DesktopSheet; return ; })} diff --git a/frontend/src/modules/common/sheeter/sheet.tsx b/frontend/src/modules/common/sheeter/sheet.tsx index a06733b2d..5e4e80478 100644 --- a/frontend/src/modules/common/sheeter/sheet.tsx +++ b/frontend/src/modules/common/sheeter/sheet.tsx @@ -1,26 +1,28 @@ import StickyBox from '~/modules/common/sticky-box'; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '~/modules/ui/sheet'; -import type { SheetT } from './state'; - +import { type SheetT, sheet as sheetState } from './state'; export interface SheetProp { sheet: SheetT; - removeSheet: () => void; + removeSheet: (sheet: SheetT) => void; } export default function DesktopSheet({ sheet, removeSheet }: SheetProp) { - const { modal = true, side = 'right', description, title, hideClose = true, className, content } = sheet; + const { id, modal = true, side = 'right', open, description, title, hideClose = true, className, content } = sheet; + const closeSheet = () => { + removeSheet(sheet); + sheet.removeCallback?.(); + }; - const handleClose = (state: boolean) => { - if (!state) { - removeSheet(); - sheet.removeCallback?.(); - } + const onOpenChange = (open: boolean) => { + if (!modal) return; + sheetState.update(id, { open }); + if (!open) closeSheet(); }; return ( - + void; @@ -16,7 +18,7 @@ export type SheetAction = { }; class SheetsStateObserver { - private sheets: SheetT[] = []; // Array to store the current sheets + public sheets: SheetT[] = []; // Array to store the current sheets private subscribers: Array<(action: SheetAction & SheetT) => void> = []; // Store subscribers that will be notified of changes // Method to subscribe to changes @@ -34,7 +36,7 @@ class SheetsStateObserver { get = (id: string) => this.sheets.find((sheet) => sheet.id === id); // Retrieve a sheet by its ID - getAll = () => this.sheets; // Retrieve a all sheets + haveOpenSheets = () => this.sheets.some((s) => s.open); // Add or update a sheet and notify subscribers set = (sheet: SheetT) => { @@ -47,10 +49,10 @@ class SheetsStateObserver { if (id) { this.sheets = this.sheets.filter((sheet) => sheet.id !== id); this.notifySubscribers({ id, remove: true }); - } else { - for (const sheet of this.sheets) this.notifySubscribers({ id: sheet.id, remove: true }); - this.sheets = []; + return; } + for (const sheet of this.sheets) this.notifySubscribers({ id: sheet.id, remove: true }); + this.sheets = []; }; // Update an existing sheet or create a new one with the provided updates @@ -65,8 +67,9 @@ class SheetsStateObserver { // Create a new sheet with the given content and optional additional data create = (content: React.ReactNode, data?: Omit) => { const modal = data?.modal || true; + const open = data?.open || true; const id = data?.id || Date.now().toString(); // Use existing ID or generate a new one - this.set({ id, modal, content, ...data }); + this.set({ id, modal, content, open, ...data }); return id; }; } @@ -78,5 +81,5 @@ export const sheet = { remove: SheetObserver.remove.bind(SheetObserver), update: SheetObserver.update.bind(SheetObserver), get: SheetObserver.get.bind(SheetObserver), - getAll: SheetObserver.getAll.bind(SheetObserver), + haveOpenSheets: SheetObserver.haveOpenSheets.bind(SheetObserver), }; diff --git a/frontend/src/modules/organizations/members-table/index.tsx b/frontend/src/modules/organizations/members-table/index.tsx index b7d1b5fb5..3f1af0377 100644 --- a/frontend/src/modules/organizations/members-table/index.tsx +++ b/frontend/src/modules/organizations/members-table/index.tsx @@ -93,16 +93,7 @@ const MembersTable = ({ entity, isSheet = false }: MembersTableProps) => { const totalCount = queryResult.data?.pages[0].total; const openUserPreview = (user: Member) => { - openUserPreviewSheet(user); - navigate({ - to: '.', - replace: true, - resetScroll: false, - search: (prev) => ({ - ...prev, - ...{ userIdPreview: user.id }, - }), - }); + openUserPreviewSheet(user, navigate, true); }; // Build columns @@ -229,7 +220,7 @@ const MembersTable = ({ entity, isSheet = false }: MembersTableProps) => { useEffect(() => { if (!rows.length || !('userIdPreview' in search) || !search.userIdPreview) return; const user = rows.find((t) => t.id === search.userIdPreview); - if (user) openUserPreviewSheet(user); + if (user) openUserPreviewSheet(user, navigate); }, [rows]); return ( diff --git a/frontend/src/modules/system/organizations-newsletter-form.tsx b/frontend/src/modules/system/organizations-newsletter-form.tsx index a4fabdfeb..714aaac01 100644 --- a/frontend/src/modules/system/organizations-newsletter-form.tsx +++ b/frontend/src/modules/system/organizations-newsletter-form.tsx @@ -12,11 +12,11 @@ import { toast } from 'sonner'; import { sendNewsletter as baseSendNewsletter } from '~/api/organizations'; import { useFormWithDraft } from '~/hooks/use-draft-form'; import { useMutation } from '~/hooks/use-mutations'; +import { BlockNote } from '~/modules/common/blocknote'; import { sheet } from '~/modules/common/sheeter/state'; import { Button } from '~/modules/ui/button'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '~/modules/ui/form'; import { Input } from '~/modules/ui/input'; -import { BlockNote } from '../common/blocknote'; interface NewsletterFormProps { organizationIds: string[]; @@ -46,7 +46,7 @@ const OrganizationsNewsletterForm: React.FC = ({ organizati form.reset(); toast.success(t('common:success.create_newsletter')); dropSelectedOrganization?.(); - if (isSheet) sheet.remove(); + if (isSheet) sheet.remove('newsletter-form'); }, }); diff --git a/frontend/src/modules/ui/sheet.tsx b/frontend/src/modules/ui/sheet.tsx index a884113dc..cdf9c543f 100644 --- a/frontend/src/modules/ui/sheet.tsx +++ b/frontend/src/modules/ui/sheet.tsx @@ -60,7 +60,10 @@ const SheetContent = React.forwardRef {children} {!hideClose && ( - + Close diff --git a/frontend/src/modules/users/users-table/columns.tsx b/frontend/src/modules/users/users-table/columns.tsx index 062b7c011..773c97b09 100644 --- a/frontend/src/modules/users/users-table/columns.tsx +++ b/frontend/src/modules/users/users-table/columns.tsx @@ -38,16 +38,7 @@ export const useColumns = (callback: (users: User[], action: 'create' | 'update' onClick={(e) => { if (e.metaKey || e.ctrlKey) return; e.preventDefault(); - navigate({ - to: '.', - replace: true, - resetScroll: false, - search: (prev) => ({ - ...prev, - ...{ userIdPreview: row.id }, - }), - }); - openUserPreviewSheet(row); + openUserPreviewSheet(row, navigate, true); }} > diff --git a/frontend/src/modules/users/users-table/index.tsx b/frontend/src/modules/users/users-table/index.tsx index 42f365f54..089cbd75d 100644 --- a/frontend/src/modules/users/users-table/index.tsx +++ b/frontend/src/modules/users/users-table/index.tsx @@ -1,5 +1,5 @@ import { onlineManager, useSuspenseInfiniteQuery } from '@tanstack/react-query'; -import { useSearch } from '@tanstack/react-router'; +import { useNavigate, useSearch } from '@tanstack/react-router'; import { useEffect, useMemo, useRef, useState } from 'react'; import { updateUser } from '~/api/users'; @@ -44,6 +44,8 @@ type SystemRoles = (typeof config.rolesByType.systemRoles)[number] | undefined; const UsersTable = () => { const { t } = useTranslation(); + const navigate = useNavigate(); + const search = useSearch({ from: UsersTableRoute.id }); const containerRef = useRef(null); @@ -169,7 +171,7 @@ const UsersTable = () => { useEffect(() => { if (!rows.length || !search.userIdPreview) return; const user = rows.find((t) => t.id === search.userIdPreview); - if (user) openUserPreviewSheet(user); + if (user) openUserPreviewSheet(user, navigate); }, [rows]); return ( From 974a509e545362af589e087ccb5168ce4ebd4090 Mon Sep 17 00:00:00 2001 From: flipvanhaaren Date: Mon, 7 Oct 2024 17:25:36 +0200 Subject: [PATCH 04/28] move prepare command to a script --- package.json | 7 +- pnpm-lock.yaml | 14 ++- prepare.js | 13 +++ scripts/list-diverged-files.sh | 156 --------------------------------- scripts/pull-upstream.sh | 125 -------------------------- 5 files changed, 31 insertions(+), 284 deletions(-) create mode 100644 prepare.js delete mode 100755 scripts/list-diverged-files.sh delete mode 100755 scripts/pull-upstream.sh diff --git a/package.json b/package.json index 9a648c471..6fdbd6013 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "access": "public" }, "scripts": { - "prepare": "cross-env [ $NODE_ENV=development ] && lefthook install && code --install-extension biomejs.biome; exit 0", + "prepare": "node prepare.js", "quick": "pnpm --filter backend run quick & turbo dev --filter frontend --filter tus", "docker": "pnpm --filter backend run docker:up --detach", "generate": "pnpm --filter backend run generate", @@ -40,5 +40,8 @@ "turbo": "^2.1.3", "typescript": "^5.6.2" }, - "packageManager": "pnpm@9.1.2" + "packageManager": "pnpm@9.1.2", + "dependencies": { + "dotenv": "^16.4.5" + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 06699e6cf..5ad367bf5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,10 @@ settings: importers: .: + dependencies: + dotenv: + specifier: ^16.4.5 + version: 16.4.5 devDependencies: '@biomejs/biome': specifier: ^1.9.3 @@ -1868,9 +1872,11 @@ packages: '@esbuild-kit/core-utils@3.3.2': resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} + deprecated: 'Merged into tsx: https://tsx.is' '@esbuild-kit/esm-loader@2.6.5': resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} + deprecated: 'Merged into tsx: https://tsx.is' '@esbuild/aix-ppc64@0.19.12': resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} @@ -2558,6 +2564,7 @@ packages: '@evilmartians/lefthook@1.7.18': resolution: {integrity: sha512-BIJBQtC7xKOedxESlB0wVG6M32ftjK+5vnID9I5CjRDE/6KcCpcK366AyG0eHGdPK1TVty81QalxZqSTxWRXPw==} + cpu: [x64, arm64, ia32] os: [darwin, linux, win32] hasBin: true @@ -9342,6 +9349,9 @@ packages: tailwind-merge@2.5.2: resolution: {integrity: sha512-kjEBm+pvD+6eAwzJL2Bi+02/9LFLal1Gs61+QB7HvTfQQ0aXwC5LGT8PEt1gS0CWKktKe6ysPTAy3cBC5MeiIg==} + tailwind-merge@2.5.3: + resolution: {integrity: sha512-d9ZolCAIzom1nf/5p4LdD5zvjmgSxY0BGgdSvmXIoMYAiPdAW/dSpP7joCDYFY7r/HkEa2qmPtkgsu0xjQeQtw==} + tailwindcss-animate@1.0.7: resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==} peerDependencies: @@ -16669,7 +16679,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-easy-sort: 1.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - tailwind-merge: 2.5.2 + tailwind-merge: 2.5.3 tsup: 6.7.0(@swc/core@1.7.26)(postcss@8.4.47)(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@22.7.4)(typescript@5.6.2))(typescript@5.6.2) transitivePeerDependencies: - '@swc/core' @@ -20499,6 +20509,8 @@ snapshots: tailwind-merge@2.5.2: {} + tailwind-merge@2.5.3: {} + tailwindcss-animate@1.0.7(tailwindcss@3.4.13(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@22.7.4)(typescript@5.6.2))): dependencies: tailwindcss: 3.4.13(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@22.7.4)(typescript@5.6.2)) diff --git a/prepare.js b/prepare.js new file mode 100644 index 000000000..13476f0dd --- /dev/null +++ b/prepare.js @@ -0,0 +1,13 @@ +import { execSync } from 'node:child_process'; +import dotenv from 'dotenv'; + +dotenv.config({ path: './backend/.env' }); + +// Install lefthook as part of prepare script +if (process.env.NODE_ENV === 'development') { + console.info('Installing lefthook & Biome VSCode extension.'); + execSync('lefthook install && code --install-extension biomejs.biome', { stdio: 'inherit' }); +} else { + console.info('Not in development. Skipping prepare script.'); + process.exit(0); +} diff --git a/scripts/list-diverged-files.sh b/scripts/list-diverged-files.sh deleted file mode 100755 index 7ac4f7c67..000000000 --- a/scripts/list-diverged-files.sh +++ /dev/null @@ -1,156 +0,0 @@ -#!/bin/bash - -# Navigate to the repository -# (e.g. if the script is in a directory called "scripts", point it to the root like: cd "$(dirname "$0")/..") -cd "$(dirname "$0")/.." # Ensures the script runs in its (root) directory - -# Path to the configuration file (can be .json, .ts, or .js) -CONFIG_FILE="cella.config.js" - -# Determine the file extension of the configuration file -FILE_EXT="${CONFIG_FILE##*.}" - -# Default variables -DIVERGED_FILE="" -IGNORE_FILE="" -IGNORE_LIST="" -UPSTREAM_BRANCH="development" # Default value set to 'development' -LOCAL_BRANCH=$(git rev-parse --abbrev-ref HEAD) - -# Function to extract paths from .json (simple key-value pairs) -extract_from_json() { - DIVERGED_FILE=$(grep '"diverged_file":' "$CONFIG_FILE" | sed 's/.*"diverged_file": *"\([^"]*\)".*/\1/') - IGNORE_FILE=$(grep '"ignore_file":' "$CONFIG_FILE" | sed 's/.*"ignore_file": *"\([^"]*\)".*/\1/') - IGNORE_LIST=$(grep '"ignore_list":' "$CONFIG_FILE" | sed 's/.*"ignore_list": *\[\([^]]*\)\].*/\1/' | tr -d '" ' | tr ',' '\n') - UPSTREAM_BRANCH=$(grep '"upstream_branch":' "$CONFIG_FILE" | sed 's/.*"upstream_branch": *"\([^"]*\)".*/\1/' || echo "$UPSTREAM_BRANCH") -} - -# Function to extract paths from .ts or .js using grep/sed -extract_from_ts() { - DIVERGED_FILE=$(grep 'divergedFile:' "$CONFIG_FILE" | sed 's/.*divergedFile: *"\([^"]*\)".*/\1/') - IGNORE_FILE=$(grep 'ignoreFile:' "$CONFIG_FILE" | sed 's/.*ignoreFile: *"\([^"]*\)".*/\1/') - IGNORE_LIST=$(grep 'ignoreList:' "$CONFIG_FILE" | sed 's/.*ignoreList: *\[\([^]]*\)\].*/\1/' | tr -d '" ' | tr ',' '\n') - UPSTREAM_BRANCH=$(grep 'upstreamBranch:' "$CONFIG_FILE" | sed 's/.*upstreamBranch: *"\([^"]*\)".*/\1/' || echo "$UPSTREAM_BRANCH") -} - -# Function to extract paths from .js or .ts using node (with dynamic import for ES modules) -extract_from_js() { - # Use node to run a script that dynamically imports the JavaScript/TypeScript configuration and outputs the values - DIVERGED_FILE=$(node -e "import('./$CONFIG_FILE').then(m => console.info(m.config.divergedFile))") - IGNORE_FILE=$(node -e "import('./$CONFIG_FILE').then(m => console.info(m.config.ignoreFile))") - IGNORE_LIST=$(node -e " - import('./$CONFIG_FILE').then(m => { - if (Array.isArray(m.config.ignoreList)) { - console.info(m.config.ignoreList.join(',')); - } else { - console.info(''); - } - }) - ") - UPSTREAM_BRANCH=$(node -e "import('./$CONFIG_FILE').then(m => console.info(m.config.upstreamBranch))" || echo "$UPSTREAM_BRANCH") -} - -# Extract values based on the file extension -if [[ "$FILE_EXT" == "json" ]]; then - # Extract from JSON (using grep for simple key-value pairs) - extract_from_json -elif [[ "$FILE_EXT" == "ts" ]]; then - # Extract from TypeScript/JavaScript - extract_from_ts -elif [[ "$FILE_EXT" == "js" ]]; then - # Extract from JavaScript (using node for dynamic imports) - extract_from_js -else - echo "Unsupported file format: $FILE_EXT. Only .json, .ts, and .js are supported." - exit 1 -fi - -# Check if the values were extracted successfully -if [[ -z "$DIVERGED_FILE" ]]; then - echo "Failed to extract diverged_file path from the configuration file." - exit 1 -fi - -# Output the variables for verification (optional) -echo "DIVERGED_FILE: $DIVERGED_FILE" -echo "UPSTREAM_BRANCH: $UPSTREAM_BRANCH" -echo "LOCAL_BRANCH: $LOCAL_BRANCH" - -# Updated echo statements for ignore files -if [ -n "$IGNORE_FILE" ]; then - echo "Ignore files by IGNORE_FILE: $IGNORE_FILE" -fi - -if [ -n "$IGNORE_LIST" ]; then - IFS=',' read -ra IGNORE_ARRAY <<< "$IGNORE_LIST" # Convert the comma-separated list to an array - echo "Ignore files by configured list (IGNORE_LIST length: ${#IGNORE_ARRAY[@]})" -fi - -# Fetch upstream changes -git fetch upstream - -# Checkout the local branch -git checkout "$LOCAL_BRANCH" - -# Get the list of tracked files from upstream branch -UPSTREAM_FILES=$(git ls-tree -r "upstream/$UPSTREAM_BRANCH" --name-only) - -# Get the list of tracked files from local branch -MAIN_FILES=$(git ls-tree -r "$LOCAL_BRANCH" --name-only) - -# Find common files between upstream and local branch -COMMON_FILES=$(comm -12 <(echo "$UPSTREAM_FILES" | sort) <(echo "$MAIN_FILES" | sort)) - -# Compare the local branch with upstream branch to get the diverged files -git diff --name-only "$LOCAL_BRANCH" "upstream/$UPSTREAM_BRANCH" > "$DIVERGED_FILE.tmp" - -# Check if the ignore list was specified directly in the config -if [[ -n "$IGNORE_LIST" ]]; then - echo "Using ignore list from config." - echo "$IGNORE_LIST" | tr ',' '\n' > "$DIVERGED_FILE.ignore.tmp" -# Otherwise, check if an ignore file was specified -elif [[ -n "$IGNORE_FILE" && -f "$IGNORE_FILE" ]]; then - echo "Using ignore file: $IGNORE_FILE" - cp "$IGNORE_FILE" "$DIVERGED_FILE.ignore.tmp" -else - echo "No ignore list or ignore file found, proceeding without ignoring files." - > "$DIVERGED_FILE.ignore.tmp" # Create an empty file -fi - -# Read the ignore patterns -IGNORE_PATTERNS=$(cat "$DIVERGED_FILE.ignore.tmp") - -# Filter diverged files: -# 1. Files must be present in both upstream and local branches -# 2. Exclude files listed in the ignore file -grep -Fxf <(echo "$COMMON_FILES") "$DIVERGED_FILE.tmp" > "$DIVERGED_FILE.tmp.filtered" - -# Now apply the ignore patterns -if [[ -n "$IGNORE_PATTERNS" ]]; then - # Create a temporary filtered file - cp "$DIVERGED_FILE.tmp.filtered" "$DIVERGED_FILE.tmp.new" - - for pattern in $IGNORE_PATTERNS; do - grep -v -E "$pattern" "$DIVERGED_FILE.tmp.new" > "$DIVERGED_FILE.tmp.filtered" - mv "$DIVERGED_FILE.tmp.filtered" "$DIVERGED_FILE.tmp.new" # Update the temporary filtered file - done - - mv "$DIVERGED_FILE.tmp.new" "$DIVERGED_FILE.tmp.filtered" # Rename back for final output -fi - -# Store the final list of diverged files in DIVERGED_FILE -mv "$DIVERGED_FILE.tmp.filtered" "$DIVERGED_FILE" - -# Check if any files were diverged and are present in both branches, but not ignored -if [[ -s "$DIVERGED_FILE" ]]; then - echo "The following files have diverged, are present in both branches, and are not ignored:" - cat "$DIVERGED_FILE" -else - echo "No files have diverged between upstream and local branch that are not ignored." - # Optionally, remove the DIVERGED_FILE if it's empty - rm -f "$DIVERGED_FILE" -fi - -# Clean up temporary files -rm -f "$DIVERGED_FILE.tmp" -rm -f "$DIVERGED_FILE.ignore.tmp" \ No newline at end of file diff --git a/scripts/pull-upstream.sh b/scripts/pull-upstream.sh deleted file mode 100755 index 548d17ecd..000000000 --- a/scripts/pull-upstream.sh +++ /dev/null @@ -1,125 +0,0 @@ -#!/bin/bash - -# Navigate to the repository -# (e.g. if the script is in a directory called "scripts", point it to the root like: cd "$(dirname "$0")/..") -cd "$(dirname "$0")/.." # Ensures the script runs in its (root) directory - -# Path to the configuration file (can be .json, .ts, or .js) -CONFIG_FILE="cella.config.js" - -# Determine the file extension of the configuration file -FILE_EXT="${CONFIG_FILE##*.}" - -# Default variables -IGNORE_FILE="" -IGNORE_LIST="" -UPSTREAM_BRANCH="development" # Default value set to 'development' -LOCAL_BRANCH=$(git rev-parse --abbrev-ref HEAD) - -# Function to extract paths from .json (simple key-value pairs) -extract_from_json() { - IGNORE_FILE=$(grep '"ignore_file":' "$CONFIG_FILE" | sed 's/.*"ignore_file": *"\([^"]*\)".*/\1/') - IGNORE_LIST=$(grep '"ignore_list":' "$CONFIG_FILE" | sed 's/.*"ignore_list": *\[\([^]]*\)\].*/\1/' | tr -d '" ' | tr ',' '\n') - UPSTREAM_BRANCH=$(grep '"upstream_branch":' "$CONFIG_FILE" | sed 's/.*"upstream_branch": *"\([^"]*\)".*/\1/' || echo "$UPSTREAM_BRANCH") -} - -# Function to extract paths from .ts or .js using grep/sed -extract_from_ts() { - IGNORE_FILE=$(grep 'ignoreFile:' "$CONFIG_FILE" | sed 's/.*ignoreFile: *"\([^"]*\)".*/\1/') - IGNORE_LIST=$(grep 'ignoreList:' "$CONFIG_FILE" | sed 's/.*ignoreList: *\[\([^]]*\)\].*/\1/' | tr -d '" ' | tr ',' '\n') - UPSTREAM_BRANCH=$(grep 'upstreamBranch:' "$CONFIG_FILE" | sed 's/.*upstreamBranch: *"\([^"]*\)".*/\1/' || echo "$UPSTREAM_BRANCH") -} - -# Function to extract paths from .js or .ts using node (with dynamic import for ES modules) -extract_from_js() { - IGNORE_FILE=$(node -e "import('./$CONFIG_FILE').then(m => console.info(m.config.ignoreFile))") - IGNORE_LIST=$(node -e "import('./$CONFIG_FILE').then(m => console.info(m.config.ignoreList.join('\n')))") - UPSTREAM_BRANCH=$(node -e "import('./$CONFIG_FILE').then(m => console.info(m.config.upstreamBranch))" || echo "$UPSTREAM_BRANCH") -} - -# Extract values based on the file extension -if [[ "$FILE_EXT" == "json" ]]; then - # Extract from JSON (using grep for simple key-value pairs) - extract_from_json -elif [[ "$FILE_EXT" == "ts" ]]; then - # Extract from TypeScript/JavaScript - extract_from_ts -elif [[ "$FILE_EXT" == "js" ]]; then - # Extract from JavaScript (using node for dynamic imports) - extract_from_js -else - echo "Unsupported file format: $FILE_EXT. Only .json, .ts, and .js are supported." - exit 1 -fi - -# Output the variables for verification (optional) -echo "UPSTREAM_BRANCH: $UPSTREAM_BRANCH" -echo "LOCAL_BRANCH: $LOCAL_BRANCH" - -# Updated echo statements for ignore files -if [ -n "$IGNORE_FILE" ]; then - echo "Ignore files by IGNORE_FILE: $IGNORE_FILE" -fi - -if [ -n "$IGNORE_LIST" ]; then - IFS=',' read -ra IGNORE_ARRAY <<< "$IGNORE_LIST" # Convert the comma-separated list to an array - echo "Ignore files by configured list (IGNORE_LIST length: ${#IGNORE_ARRAY[@]})" -fi - -# Fetch upstream changes -git fetch upstream - -# Checkout the local branch -git checkout "$LOCAL_BRANCH" - -# Merge upstream changes without committing -git merge --no-commit "upstream/$UPSTREAM_BRANCH" - -# If neither `ignoreList` nor `ignoreFile` exists or is empty, skip the reset/checkout process -if [[ -n "$IGNORE_LIST" || ( -f "$IGNORE_FILE" && -s "$IGNORE_FILE" ) ]]; then - echo "Applying reset/checkout based on ignoreList or ignoreFile..." - - # If `ignoreList` is provided, use it; otherwise, fall back to `ignoreFile` - if [[ -n "$IGNORE_LIST" ]]; then - # Loop through each pattern in the ignoreList and process it - for pattern in $IGNORE_LIST; do - for file in $(git ls-files); do - if [[ "$file" == "$pattern" ]]; then - echo "Keeping $file from current branch" - git reset "$file" - git checkout --ours -- "$file" - fi - done - done - elif [[ -f "$IGNORE_FILE" ]]; then - # Process each line in the ignoreFile - while IFS= read -r pattern || [[ -n "$pattern" ]]; do - for file in $(git ls-files); do - if [[ "$file" == "$pattern" ]]; then - echo "Keeping $file from current branch" - git reset "$file" - git checkout --ours -- "$file" - fi - done - done < "$IGNORE_FILE" - fi -else - echo "No files to ignore. Skipping reset/checkout." -fi - -# Check for merge conflicts -if git diff --check > /dev/null; then - echo "No merge conflicts detected, proceeding with commit." - - # Stage the changes - git add . - - # Commit the merge - git commit -m "Merged upstream changes, keeping files listed in $IGNORE_FILE." - - # Push changes to your fork - # git push origin branch -else - echo "Merge conflicts detected. Resolve conflicts before committing." - exit 1 -fi \ No newline at end of file From 074268f3fc32bb8bd2522f09d88ceff8d1d410e2 Mon Sep 17 00:00:00 2001 From: flipvanhaaren Date: Mon, 7 Oct 2024 19:14:58 +0200 Subject: [PATCH 05/28] code readility --- backend/src/modules/memberships/index.ts | 22 +++--- .../sheet-menu-options/item-option.tsx | 79 ++++++++++--------- 2 files changed, 52 insertions(+), 49 deletions(-) diff --git a/backend/src/modules/memberships/index.ts b/backend/src/modules/memberships/index.ts index 4bf584455..c23502653 100644 --- a/backend/src/modules/memberships/index.ts +++ b/backend/src/modules/memberships/index.ts @@ -1,4 +1,4 @@ -import { type SQL, and, count, desc, eq, ilike, inArray, or } from 'drizzle-orm'; +import { type SQL, and, count, eq, ilike, inArray, or } from 'drizzle-orm'; import { db } from '#/db/db'; import { type MembershipModel, membershipSelect, membershipsTable } from '#/db/schema/memberships'; @@ -323,18 +323,16 @@ const membershipsRoutes = app const updatedType = membershipToUpdate.type; const updatedEntityIdField = entityIdFields[updatedType]; - // on restore item set last order in memberships - if (archived === false) { - const [lastOrderMembership] = await db - .select() - .from(membershipsTable) - .where(and(eq(membershipsTable.type, updatedType), eq(membershipsTable.userId, user.id))) - .orderBy(desc(membershipsTable.order)) - .limit(1); - const ceilOrder = Math.ceil(lastOrderMembership.order); - orderToUpdate = lastOrderMembership.order === ceilOrder ? ceilOrder + 1 : ceilOrder; - } + // if archived changed, set lowest order in relevant memberships + if (archived !== undefined && archived !== membershipToUpdate.archived) { + const relevantMemberships = memberships.filter((membership) => membership.type === updatedType && membership.archived === archived); + const lastOrderMembership = relevantMemberships.sort((a, b) => b.order - a.order)[0]; + + const ceilOrder = lastOrderMembership ? Math.ceil(lastOrderMembership.order) : 0; + + orderToUpdate = ceilOrder + 1; + } const membershipContext = await resolveEntity(updatedType, membershipToUpdate[updatedEntityIdField]); if (!membershipContext) return errorResponse(ctx, 404, 'not_found', 'warn', updatedType); diff --git a/frontend/src/modules/common/nav-sheet/sheet-menu-options/item-option.tsx b/frontend/src/modules/common/nav-sheet/sheet-menu-options/item-option.tsx index bfa9aede8..9ffb3eebb 100644 --- a/frontend/src/modules/common/nav-sheet/sheet-menu-options/item-option.tsx +++ b/frontend/src/modules/common/nav-sheet/sheet-menu-options/item-option.tsx @@ -1,8 +1,8 @@ import { onlineManager } from '@tanstack/react-query'; import type { ContextEntity } from 'backend/types/common'; +import { config } from 'config'; import { motion } from 'framer-motion'; -import { Archive, ArchiveRestore, Bell, BellOff, Loader2 } from 'lucide-react'; -import { useState } from 'react'; +import { Archive, ArchiveRestore, Bell, BellOff } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { toast } from 'sonner'; import { type UpdateMenuOptionsProp, updateMembership as baseUpdateMembership } from '~/api/memberships'; @@ -12,6 +12,7 @@ import { AvatarWrap } from '~/modules/common/avatar-wrap'; import { Button } from '~/modules/ui/button'; import { useNavigationStore } from '~/store/navigation'; import type { UserMenuItem } from '~/types/common'; +import Spinner from '../../spinner'; interface ItemOptionProps { item: UserMenuItem; @@ -21,55 +22,57 @@ interface ItemOptionProps { export const ItemOption = ({ item, itemType, parentItemSlug }: ItemOptionProps) => { const { t } = useTranslation(); - const [isItemArchived, setItemArchived] = useState(item.membership.archived); - const [isItemMuted, setItemMuted] = useState(item.membership.muted); - const { archiveStateToggle } = useNavigationStore(); const { mutate: updateMembership, status } = useMutation({ - mutationFn: (values: UpdateMenuOptionsProp) => { - return baseUpdateMembership(values); - }, + mutationFn: (values: UpdateMenuOptionsProp) => baseUpdateMembership(values), onSuccess: (updatedMembership) => { - let toast: string | undefined; - if (updatedMembership.archived !== isItemArchived) { - const archived = updatedMembership.archived || !isItemArchived; + let toastMessage: string | undefined; + + if (updatedMembership.archived !== item.membership.archived) { + const archived = updatedMembership.archived || !item.membership.archived; archiveStateToggle(item, archived, parentItemSlug ? parentItemSlug : null); - setItemArchived(archived); - toast = t(`common:success.${updatedMembership.archived ? 'archived' : 'restore'}_resource`, { resource: t(`common:${itemType}`) }); + item.membership.archived = archived; + toastMessage = t(`common:success.${updatedMembership.archived ? 'archived' : 'restore'}_resource`, { resource: t(`common:${itemType}`) }); } - if (updatedMembership.muted !== isItemMuted) { - const muted = updatedMembership.muted || !isItemMuted; - setItemMuted(muted); - toast = t(`common:success.${updatedMembership.muted ? 'mute' : 'unmute'}_resource`, { resource: t(`common:${itemType}`) }); + + if (updatedMembership.muted !== item.membership.muted) { + const muted = updatedMembership.muted || !item.membership.muted; + item.membership.muted = muted; + toastMessage = t(`common:success.${updatedMembership.muted ? 'mute' : 'unmute'}_resource`, { resource: t(`common:${itemType}`) }); } - // Triggers an event to handle actions for menu entities that have been changed (default listens in nav-sheet/index) - dispatchCustomEvent('menuEntityChange', { entity: itemType, membership: updatedMembership, toast }); + + dispatchCustomEvent('menuEntityChange', { entity: itemType, membership: updatedMembership, toast: toastMessage }); }, }); - const itemOptionStatesHandle = (state: 'archive' | 'mute') => { - if (!onlineManager.isOnline()) return toast.warning(t('common:action.offline.text')); + const handleUpdateMembershipKey = (key: 'archive' | 'mute') => { + if (!onlineManager.isOnline()) { + toast.warning(t('common:action.offline.text')); + return; + } - const role = item.membership.role; - const membershipId = item.membership.id; - const organizationId = item.organizationId || item.id; + const { role, id: membershipId, organizationId, archived, muted } = item.membership; + const membership = { membershipId, role, muted, archived, organizationId }; - if (state === 'archive') updateMembership({ membershipId, role, archived: !isItemArchived, organizationId }); - if (state === 'mute') updateMembership({ membershipId, role, muted: !isItemMuted, organizationId }); + if (key === 'archive') membership.archived = !item.membership.archived; + if (key === 'mute') membership.muted = !item.membership.muted; + updateMembership(membership); }; return ( {status === 'pending' ? ( - +
    + +
    ) : ( -
    {item.name}
    +
    + {item.name} {config.mode === 'development' && #{item.membership.order}} +
    itemOptionStatesHandle('archive')} + Icon={item.membership.archived ? ArchiveRestore : Archive} + title={item.membership.archived ? t('common:restore') : t('common:archive')} + onClick={() => handleUpdateMembershipKey('archive')} subTask={!!parentItemSlug} /> itemOptionStatesHandle('mute')} + Icon={item.membership.muted ? Bell : BellOff} + title={item.membership.muted ? t('common:unmute') : t('common:mute')} + onClick={() => handleUpdateMembershipKey('mute')} subTask={!!parentItemSlug} />
    From 64282b515e88cb8b9385d660448bd2f47e0e4b4d Mon Sep 17 00:00:00 2001 From: flipvanhaaren Date: Mon, 7 Oct 2024 19:26:21 +0200 Subject: [PATCH 06/28] simpler to just enable sorting for archived too --- frontend/src/modules/common/nav-sheet/helpers/index.ts | 10 +++++----- .../nav-sheet/sheet-menu-options/item-option.tsx | 3 +-- frontend/src/modules/common/nav-sheet/sheet-menu.tsx | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/frontend/src/modules/common/nav-sheet/helpers/index.ts b/frontend/src/modules/common/nav-sheet/helpers/index.ts index 047052c08..65f3ec28e 100644 --- a/frontend/src/modules/common/nav-sheet/helpers/index.ts +++ b/frontend/src/modules/common/nav-sheet/helpers/index.ts @@ -1,20 +1,20 @@ import type { ContextEntity, UserMenu, UserMenuItem } from '~/types/common'; -const sortAndFilterMenu = (data: UserMenuItem[], entityType: ContextEntity) => { +const sortAndFilterMenu = (data: UserMenuItem[], entityType: ContextEntity, archived: boolean) => { const menuList = data //filter by type and archive state - .filter((el) => el.entity === entityType && !el.membership.archived) + .filter((el) => el.entity === entityType && el.membership.archived === archived) // sort items by order .sort((a, b) => a.membership.order - b.membership.order); return menuList; }; -export const findRelatedItemsByType = (data: UserMenu, entityType: ContextEntity) => { +export const findRelatedItemsByType = (data: UserMenu, entityType: ContextEntity, archived: boolean) => { const flatData = Object.values(data).flat(); - const items = sortAndFilterMenu(flatData, entityType); + const items = sortAndFilterMenu(flatData, entityType, archived); if (items.length) return items; const subItemsMenu = flatData.flatMap((el) => el.submenu || []); - const subItems = sortAndFilterMenu(subItemsMenu, entityType); + const subItems = sortAndFilterMenu(subItemsMenu, entityType, archived); return subItems.length ? subItems : []; }; diff --git a/frontend/src/modules/common/nav-sheet/sheet-menu-options/item-option.tsx b/frontend/src/modules/common/nav-sheet/sheet-menu-options/item-option.tsx index 9ffb3eebb..e68de67d9 100644 --- a/frontend/src/modules/common/nav-sheet/sheet-menu-options/item-option.tsx +++ b/frontend/src/modules/common/nav-sheet/sheet-menu-options/item-option.tsx @@ -63,8 +63,7 @@ export const ItemOption = ({ item, itemType, parentItemSlug }: ItemOptionProps) {status === 'pending' ? (
    diff --git a/frontend/src/modules/common/nav-sheet/sheet-menu.tsx b/frontend/src/modules/common/nav-sheet/sheet-menu.tsx index 3d77e0a8d..e5776ddae 100644 --- a/frontend/src/modules/common/nav-sheet/sheet-menu.tsx +++ b/frontend/src/modules/common/nav-sheet/sheet-menu.tsx @@ -89,7 +89,7 @@ export const SheetMenu = memo(() => { if (!isPageData(targetData) || !isPageData(sourceData)) return; const closestEdgeOfTarget: Edge | null = extractClosestEdge(targetData); - const neededItems = findRelatedItemsByType(menu, sourceData.item.entity); + const neededItems = findRelatedItemsByType(menu, sourceData.item.entity, sourceData.item.membership.archived); const targetItemIndex = neededItems.findIndex((i) => i.id === targetData.item.id); const relativeItemIndex = closestEdgeOfTarget === 'top' ? targetItemIndex - 1 : targetItemIndex + 1; From d4637bd0fdaa7f5910dd0e21d59765df47895ff3 Mon Sep 17 00:00:00 2001 From: flipvanhaaren Date: Tue, 8 Oct 2024 09:31:26 +0200 Subject: [PATCH 07/28] fix double container in profile page content --- frontend/src/modules/users/profile-page-content.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/frontend/src/modules/users/profile-page-content.tsx b/frontend/src/modules/users/profile-page-content.tsx index 968b04153..74ea3f5dc 100644 --- a/frontend/src/modules/users/profile-page-content.tsx +++ b/frontend/src/modules/users/profile-page-content.tsx @@ -22,11 +22,7 @@ const ProfilePageContent = ({ sheet, userId, organizationId }: { userId: string; console.debug('data available in profile page content:', sheet, userId, organizationId); - return ( -
    - -
    - ); + return ; }; export default ProfilePageContent; From 6558b5685f3d7ec5c6fd908ad35de2bde11e1cdd Mon Sep 17 00:00:00 2001 From: flipvanhaaren Date: Tue, 8 Oct 2024 10:35:54 +0200 Subject: [PATCH 08/28] update deps --- frontend/package.json | 10 +++--- pnpm-lock.yaml | 71 +++++++++++++++++++++---------------------- 2 files changed, 40 insertions(+), 41 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 48a6b667d..df0ca2f47 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,9 +17,9 @@ "dependencies": { "@atlaskit/pragmatic-drag-and-drop": "^1.3.1", "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3", - "@blocknote/core": "^0.15.11", - "@blocknote/react": "^0.15.11", - "@blocknote/shadcn": "^0.15.11", + "@blocknote/core": "^0.16.0", + "@blocknote/react": "^0.16.0", + "@blocknote/shadcn": "^0.16.0", "@github/mini-throttle": "^2.1.1", "@hookform/resolvers": "^3.9.0", "@oslojs/encoding": "^1.1.0", @@ -53,8 +53,8 @@ "@tanstack/react-query": "^5.59.0", "@tanstack/react-query-devtools": "^5.59.0", "@tanstack/react-query-persist-client": "^5.59.0", - "@tanstack/react-router": "^1.58.17", - "@tanstack/router-devtools": "^1.58.17", + "@tanstack/react-router": "^1.63.2", + "@tanstack/router-devtools": "^1.63.2", "@uppy/audio": "^2.0.1", "@uppy/core": "^4.2.1", "@uppy/dashboard": "^4.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5ad367bf5..81b322c0d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -301,14 +301,14 @@ importers: specifier: ^1.0.3 version: 1.0.3 '@blocknote/core': - specifier: ^0.15.11 - version: 0.15.11 + specifier: ^0.16.0 + version: 0.16.0 '@blocknote/react': - specifier: ^0.15.11 - version: 0.15.11(@tiptap/pm@2.8.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^0.16.0 + version: 0.16.0(@tiptap/pm@2.8.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@blocknote/shadcn': - specifier: ^0.15.11 - version: 0.15.11(@tiptap/pm@2.8.0)(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@22.7.4)(typescript@5.6.2)) + specifier: ^0.16.0 + version: 0.16.0(@tiptap/pm@2.8.0)(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@22.7.4)(typescript@5.6.2)) '@github/mini-throttle': specifier: ^2.1.1 version: 2.1.1 @@ -409,11 +409,11 @@ importers: specifier: ^5.59.0 version: 5.59.0(@tanstack/react-query@5.59.0(react@18.3.1))(react@18.3.1) '@tanstack/react-router': - specifier: ^1.58.17 - version: 1.58.17(@tanstack/router-generator@1.58.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^1.63.2 + version: 1.63.2(@tanstack/router-generator@1.58.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/router-devtools': - specifier: ^1.58.17 - version: 1.58.17(@tanstack/react-router@1.58.17(@tanstack/router-generator@1.58.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^1.63.2 + version: 1.63.2(@tanstack/react-router@1.63.2(@tanstack/router-generator@1.58.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@uppy/audio': specifier: ^2.0.1 version: 2.0.1(@uppy/core@4.2.1) @@ -1556,17 +1556,17 @@ packages: cpu: [x64] os: [win32] - '@blocknote/core@0.15.11': - resolution: {integrity: sha512-IVOypyKkqZxONLVCc2Q2+NBvFCzmh57IrdZDOhWtIroq2qfAgvPp+aVoFlU2EZK9scJwFMqzarHHHpBxl+5f1A==} + '@blocknote/core@0.16.0': + resolution: {integrity: sha512-egX+GjlAB8r/zaox278zNTTUMNVRHVQ2qVlPHQZgGOXSDq2Z+Lm7i4xKYMz/UT/IdrL7iGxnHrAsbc0H/kqc9A==} - '@blocknote/react@0.15.11': - resolution: {integrity: sha512-HGNvVW80pZd+qhHhgYM9O/qGRrHRLxr4pAju78tK/treUzX8qV+uOV9IPMBfhASzizLfsLCWJC95XGCEKBJrFQ==} + '@blocknote/react@0.16.0': + resolution: {integrity: sha512-vEwAp4z1FBqcH75OEbEW/yd4nj8XcSKAzCElV7aL6nVhPiKgYzrzG/WVckTq1h9lMaGeAuYqLErww4IIsbiawg==} peerDependencies: react: ^18 react-dom: ^18 - '@blocknote/shadcn@0.15.11': - resolution: {integrity: sha512-VuaRMIWWU2cYjo+14exwKb0Qz01scsSxL2tS4Eo2pxHoFzDdxlCteCrWtAaldpZAnJJDkM3X279U3BICmxDLPA==} + '@blocknote/shadcn@0.16.0': + resolution: {integrity: sha512-4mqvtvZgFBKyFtYrgWWokP+SbkRjaZJsEMsxKlnVb+XlYkT+KKzgvYjXQioySwcNOMhgAjOWK2bleZ7CfPT1Ww==} peerDependencies: react: ^18 react-dom: ^18 @@ -2564,7 +2564,6 @@ packages: '@evilmartians/lefthook@1.7.18': resolution: {integrity: sha512-BIJBQtC7xKOedxESlB0wVG6M32ftjK+5vnID9I5CjRDE/6KcCpcK366AyG0eHGdPK1TVty81QalxZqSTxWRXPw==} - cpu: [x64, arm64, ia32] os: [darwin, linux, win32] hasBin: true @@ -4836,8 +4835,8 @@ packages: peerDependencies: tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20' - '@tanstack/history@1.58.15': - resolution: {integrity: sha512-M36Ke2Q2v8Iv4Cx0xw04iVkixuOligiFLOifH35DqGnzXe9PAtTHIooieQowqYkAjC09KuLo5j6sgvwKTZ+U5Q==} + '@tanstack/history@1.61.1': + resolution: {integrity: sha512-2CqERleeqO3hkhJmyJm37tiL3LYgeOpmo8szqdjgtnnG0z7ZpvzkZz6HkfOr9Ca/ha7mhAiouSvLYuLkM37AMg==} engines: {node: '>=12'} '@tanstack/query-core@5.59.0': @@ -4869,8 +4868,8 @@ packages: peerDependencies: react: ^18 || ^19 - '@tanstack/react-router@1.58.17': - resolution: {integrity: sha512-t9QDunhRywq043ArnwWo6OgJh99jHkTJRQS+Yl25CLalfbi7qyNvTXSQuAzwIKz0GK0JjkD4eGf7ZtTayaOZ1w==} + '@tanstack/react-router@1.63.2': + resolution: {integrity: sha512-6Bla8LK4cu4L0atBZNNIUhQpiCgXNIa2XnrybHVbqTipWlfRJ0I71pXWuSVqGP487wNGZgVG5ITZFLR+y+NhKg==} engines: {node: '>=12'} peerDependencies: '@tanstack/router-generator': 1.58.12 @@ -4886,11 +4885,11 @@ packages: react: ^17.0.0 || ^18.0.0 react-dom: ^17.0.0 || ^18.0.0 - '@tanstack/router-devtools@1.58.17': - resolution: {integrity: sha512-VdvVldHigm9E49Q0FOSz2mLqf67SY7F8TF3TAbLlr06iI99QBMcnEU7u+pGsO/wlX8hSirliX1Lbj+9zDY57Ow==} + '@tanstack/router-devtools@1.63.2': + resolution: {integrity: sha512-qNtjl6fbgjybVyvxPNHgx91F2JqiWLIuIoCTLVN6yX/WRGH6SfWT4SxEfWGYieHTT9YX8fsFo44F15WeStFHVg==} engines: {node: '>=12'} peerDependencies: - '@tanstack/react-router': ^1.58.17 + '@tanstack/react-router': ^1.63.2 react: '>=18' react-dom: '>=18' @@ -11513,7 +11512,7 @@ snapshots: '@biomejs/cli-win32-x64@1.9.3': optional: true - '@blocknote/core@0.15.11': + '@blocknote/core@0.16.0': dependencies: '@emoji-mart/data': 1.2.1 '@tiptap/core': 2.8.0(@tiptap/pm@2.8.0) @@ -11559,9 +11558,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@blocknote/react@0.15.11(@tiptap/pm@2.8.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@blocknote/react@0.16.0(@tiptap/pm@2.8.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@blocknote/core': 0.15.11 + '@blocknote/core': 0.16.0 '@floating-ui/react': 0.26.24(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tiptap/core': 2.8.0(@tiptap/pm@2.8.0) '@tiptap/react': 2.8.0(@tiptap/core@2.8.0(@tiptap/pm@2.8.0))(@tiptap/pm@2.8.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -11573,10 +11572,10 @@ snapshots: - '@tiptap/pm' - supports-color - '@blocknote/shadcn@0.15.11(@tiptap/pm@2.8.0)(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@22.7.4)(typescript@5.6.2))': + '@blocknote/shadcn@0.16.0(@tiptap/pm@2.8.0)(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@22.7.4)(typescript@5.6.2))': dependencies: - '@blocknote/core': 0.15.11 - '@blocknote/react': 0.15.11(@tiptap/pm@2.8.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@blocknote/core': 0.16.0 + '@blocknote/react': 0.16.0(@tiptap/pm@2.8.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@hookform/resolvers': 3.9.0(react-hook-form@7.53.0(react@18.3.1)) '@radix-ui/react-dropdown-menu': 2.1.2(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-label': 2.1.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -11594,7 +11593,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-hook-form: 7.53.0(react@18.3.1) - tailwind-merge: 2.5.2 + tailwind-merge: 2.5.3 tailwindcss: 3.4.13(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@22.7.4)(typescript@5.6.2)) tailwindcss-animate: 1.0.7(tailwindcss@3.4.13(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@22.7.4)(typescript@5.6.2))) zod: 3.23.8 @@ -15065,7 +15064,7 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 3.4.13(ts-node@10.9.2(@swc/core@1.7.26)(@types/node@22.7.4)(typescript@5.6.2)) - '@tanstack/history@1.58.15': {} + '@tanstack/history@1.61.1': {} '@tanstack/query-core@5.59.0': {} @@ -15097,9 +15096,9 @@ snapshots: '@tanstack/query-core': 5.59.0 react: 18.3.1 - '@tanstack/react-router@1.58.17(@tanstack/router-generator@1.58.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@tanstack/react-router@1.63.2(@tanstack/router-generator@1.58.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@tanstack/history': 1.58.15 + '@tanstack/history': 1.61.1 '@tanstack/react-store': 0.5.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -15115,9 +15114,9 @@ snapshots: react-dom: 18.3.1(react@18.3.1) use-sync-external-store: 1.2.2(react@18.3.1) - '@tanstack/router-devtools@1.58.17(@tanstack/react-router@1.58.17(@tanstack/router-generator@1.58.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@tanstack/router-devtools@1.63.2(@tanstack/react-router@1.63.2(@tanstack/router-generator@1.58.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@tanstack/react-router': 1.58.17(@tanstack/router-generator@1.58.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tanstack/react-router': 1.63.2(@tanstack/router-generator@1.58.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) clsx: 2.1.1 goober: 2.1.14(csstype@3.1.3) react: 18.3.1 From c6ce33808efed0e6b7b0216b357c585e2efa203b Mon Sep 17 00:00:00 2001 From: flipvanhaaren Date: Tue, 8 Oct 2024 11:14:37 +0200 Subject: [PATCH 09/28] clean up --- cella.config.js | 1 + cli/create-cella/package.json | 16 +--------------- cli/sync-cella/package.json | 19 +------------------ frontend/src/modules/common/main-search.tsx | 7 ++++--- .../src/modules/common/query-combobox.tsx | 7 ++++--- 5 files changed, 11 insertions(+), 39 deletions(-) diff --git a/cella.config.js b/cella.config.js index 6e1470ebe..efc985bc1 100644 --- a/cella.config.js +++ b/cella.config.js @@ -26,6 +26,7 @@ export const config = { 'config/default.ts', 'config/development.ts', 'config/tunnel.ts', + 'cli/create-cella/*', 'frontend/vite.config.ts', 'frontend/public/favicon.ico', 'frontend/public/static/icons/*', diff --git a/cli/create-cella/package.json b/cli/create-cella/package.json index 53c6409ed..71bc25b6e 100644 --- a/cli/create-cella/package.json +++ b/cli/create-cella/package.json @@ -3,21 +3,7 @@ "version": "0.0.3", "private": false, "license": "MIT", - "description": "Intuivive TypeScript template to build local-first web apps. Implementation-ready. MIT license.", - "keywords": [ - "template", - "monorepo", - "fullstack", - "typescript", - "hono", - "drizzle", - "shadcn", - "postgres", - "react", - "vite", - "pwa", - "cli" - ], + "description": "Create your own app in seconds with Cella: a TypeScript template for local-first web apps.", "publishConfig": { "access": "public" }, diff --git a/cli/sync-cella/package.json b/cli/sync-cella/package.json index f9b29090a..a6f3c46a4 100644 --- a/cli/sync-cella/package.json +++ b/cli/sync-cella/package.json @@ -3,24 +3,7 @@ "version": "0.0.1", "private": false, "license": "MIT", - "description": "Intuivive TypeScript template to build local-first web apps. Implementation-ready. MIT license.", - "keywords": [ - "template", - "monorepo", - "fullstack", - "typescript", - "hono", - "drizzle", - "shadcn", - "postgres", - "react", - "vite", - "pwa", - "cli", - "sync", - "fetch", - "upstream" - ], + "description": "Receive updates from cella while reducing conflicts.", "repository": { "type": "git", "url": "https://github.com/cellajs/cella", diff --git a/frontend/src/modules/common/main-search.tsx b/frontend/src/modules/common/main-search.tsx index 8248d445d..12abb4c15 100644 --- a/frontend/src/modules/common/main-search.tsx +++ b/frontend/src/modules/common/main-search.tsx @@ -3,7 +3,7 @@ import { useNavigate } from '@tanstack/react-router'; import type { entitySuggestionSchema } from 'backend/modules/general/schema'; import type { Entity } from 'backend/types/common'; import { config } from 'config'; -import { History, Loader2, Search, X } from 'lucide-react'; +import { History, Search, X } from 'lucide-react'; import { Fragment, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import type { z } from 'zod'; @@ -17,6 +17,7 @@ import { ScrollArea } from '~/modules/ui/scroll-area'; import { baseEntityRoutes, suggestionSections } from '~/nav-config'; import { useNavigationStore } from '~/store/navigation'; import { Button } from '../ui/button'; +import Spinner from './spinner'; type SuggestionType = z.infer; @@ -116,8 +117,8 @@ export const MainSearch = () => { /> {isFetching && ( - - + + )} { diff --git a/frontend/src/modules/common/query-combobox.tsx b/frontend/src/modules/common/query-combobox.tsx index 2affd6af7..3fb39e973 100644 --- a/frontend/src/modules/common/query-combobox.tsx +++ b/frontend/src/modules/common/query-combobox.tsx @@ -1,4 +1,4 @@ -import { Check, ChevronsUpDown, Loader2, Search, X } from 'lucide-react'; +import { Check, ChevronsUpDown, Search, X } from 'lucide-react'; import { useQuery } from '@tanstack/react-query'; import { config } from 'config'; @@ -13,6 +13,7 @@ import { Badge } from '~/modules/ui/badge'; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandLoading } from '~/modules/ui/command'; import { Popover, PopoverContent, PopoverTrigger } from '~/modules/ui/popover'; import { ScrollArea } from '~/modules/ui/scroll-area'; +import Spinner from './spinner'; export function QueryCombobox({ onChange, value }: { value: string[]; onChange: (items: string[]) => void }) { const { t } = useTranslation(); @@ -106,8 +107,8 @@ export function QueryCombobox({ onChange, value }: { value: string[]; onChange: placeholder={t('common:placeholder.type_name')} /> {isLoading && ( - - + + )} From a61feb4f5f7596d212072e5c0ac2fe5cacbe8fd7 Mon Sep 17 00:00:00 2001 From: flipvanhaaren Date: Wed, 9 Oct 2024 11:15:54 +0200 Subject: [PATCH 10/28] tuning --- frontend/src/modules/common/data-table/util.tsx | 7 ++----- .../modules/common/main-nav/float-nav/button-container.tsx | 6 +++--- frontend/src/modules/ui/dialog.tsx | 2 +- frontend/src/modules/ui/sheet.tsx | 2 +- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/frontend/src/modules/common/data-table/util.tsx b/frontend/src/modules/common/data-table/util.tsx index 7e8b738ab..577976a68 100644 --- a/frontend/src/modules/common/data-table/util.tsx +++ b/frontend/src/modules/common/data-table/util.tsx @@ -31,11 +31,8 @@ export const openUserPreviewSheet = (user: Omit, navigate: Navig replace: true, resetScroll: false, search: (prev) => { - const newSearch = { ...prev }; - for (const key of objectKeys(newSearch)) { - if (key.includes('Preview')) delete newSearch[key]; - } - return newSearch; + const { userIdPreview: _, ...nextSearch } = prev; + return nextSearch; }, }); sheet.remove(`user-preview-${user.id}`); diff --git a/frontend/src/modules/common/main-nav/float-nav/button-container.tsx b/frontend/src/modules/common/main-nav/float-nav/button-container.tsx index 57d2da27a..046b8f4c5 100644 --- a/frontend/src/modules/common/main-nav/float-nav/button-container.tsx +++ b/frontend/src/modules/common/main-nav/float-nav/button-container.tsx @@ -15,12 +15,12 @@ const MobileNavButton: React.FC = ({ Icon, onClick, classN return ( ); }; diff --git a/frontend/src/modules/ui/dialog.tsx b/frontend/src/modules/ui/dialog.tsx index d7ac07aaa..570bc312c 100644 --- a/frontend/src/modules/ui/dialog.tsx +++ b/frontend/src/modules/ui/dialog.tsx @@ -46,7 +46,7 @@ const DialogContent = React.forwardRef< > {children} {!hideClose && ( - + Close diff --git a/frontend/src/modules/ui/sheet.tsx b/frontend/src/modules/ui/sheet.tsx index cdf9c543f..bf91529d3 100644 --- a/frontend/src/modules/ui/sheet.tsx +++ b/frontend/src/modules/ui/sheet.tsx @@ -62,7 +62,7 @@ const SheetContent = React.forwardRef Close From 565ca014241f03ac2dcf6df35186c11bddfade66 Mon Sep 17 00:00:00 2001 From: flipvanhaaren Date: Wed, 9 Oct 2024 11:16:48 +0200 Subject: [PATCH 11/28] + --- frontend/src/modules/common/data-table/util.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/modules/common/data-table/util.tsx b/frontend/src/modules/common/data-table/util.tsx index 577976a68..d69508646 100644 --- a/frontend/src/modules/common/data-table/util.tsx +++ b/frontend/src/modules/common/data-table/util.tsx @@ -2,7 +2,6 @@ import type { NavigateFn } from '@tanstack/react-router'; import { Suspense, lazy } from 'react'; import { sheet } from '~/modules/common/sheeter/state'; import type { User } from '~/types/common'; -import { objectKeys } from '~/utils/object'; const UserProfilePage = lazy(() => import('~/modules/users/profile-page')); From 854d716328993744f30a01dfd84e1fe15d153f3a Mon Sep 17 00:00:00 2001 From: flipvanhaaren Date: Wed, 9 Oct 2024 11:45:38 +0200 Subject: [PATCH 12/28] remove console logs --- frontend/src/hooks/use-body-class.ts | 15 +++------------ frontend/src/routes/general.tsx | 4 +++- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/frontend/src/hooks/use-body-class.ts b/frontend/src/hooks/use-body-class.ts index 6df59f688..aceebad99 100644 --- a/frontend/src/hooks/use-body-class.ts +++ b/frontend/src/hooks/use-body-class.ts @@ -12,15 +12,9 @@ function useBodyClass(classMappings: { [key: string]: boolean }) { for (let i = 0; i < classNames.length; i++) { const className = classNames[i]; if (stableClassMappings[className]) { - if (!bodyClassList.contains(className)) { - console.log(`Adding class: ${className}`); - bodyClassList.add(className); - } + if (!bodyClassList.contains(className)) bodyClassList.add(className); } else { - if (bodyClassList.contains(className)) { - console.log(`Removing class: ${className}`); - bodyClassList.remove(className); - } + if (bodyClassList.contains(className)) bodyClassList.remove(className); } } @@ -28,10 +22,7 @@ function useBodyClass(classMappings: { [key: string]: boolean }) { return () => { for (let i = 0; i < classNames.length; i++) { const className = classNames[i]; - if (bodyClassList.contains(className)) { - console.log(`Cleaning up class: ${className}`); - bodyClassList.remove(className); - } + if (bodyClassList.contains(className)) bodyClassList.remove(className); } }; }, [stableClassMappings]); diff --git a/frontend/src/routes/general.tsx b/frontend/src/routes/general.tsx index 7684c2f6b..ad8cc6185 100644 --- a/frontend/src/routes/general.tsx +++ b/frontend/src/routes/general.tsx @@ -65,7 +65,9 @@ export const AppRoute = createRoute({ ), - loader: async ({ location }) => { + loader: async ({ location, cause }) => { + if (cause !== 'enter') return; + try { console.debug('Fetch me & menu while entering app ', location.pathname); const getSelf = async () => { From ffb3ee788208b4afa503520b8963043def71bd1e Mon Sep 17 00:00:00 2001 From: flipvanhaaren Date: Wed, 9 Oct 2024 12:44:50 +0200 Subject: [PATCH 13/28] make nav bar logo animation configurable --- config/default.ts | 3 +++ .../common/main-nav/bar-nav/bar-nav-loader.tsx | 2 +- frontend/tailwind.config.ts | 16 ++++++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/config/default.ts b/config/default.ts index 3c67299ef..d89729e46 100644 --- a/config/default.ts +++ b/config/default.ts @@ -164,6 +164,9 @@ export const config = { }, }, + // UI settings + navLogoAnimation: 'animate-spin-slow', + // Common countries common: { countries: ['fr', 'de', 'nl', 'ua', 'us', 'gb'], diff --git a/frontend/src/modules/common/main-nav/bar-nav/bar-nav-loader.tsx b/frontend/src/modules/common/main-nav/bar-nav/bar-nav-loader.tsx index 47a577227..c91cb5538 100644 --- a/frontend/src/modules/common/main-nav/bar-nav/bar-nav-loader.tsx +++ b/frontend/src/modules/common/main-nav/bar-nav/bar-nav-loader.tsx @@ -17,7 +17,7 @@ const MainNavLoader = () => { <> diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts index 188b74ba7..359f34a7f 100644 --- a/frontend/tailwind.config.ts +++ b/frontend/tailwind.config.ts @@ -100,6 +100,19 @@ export default { '60%': { transform: 'rotate(0.0deg)' }, '100%': { transform: 'rotate(0.0deg)' }, }, + heartbeat: { + '0%': { transform: 'scale(1);' }, + '14%': { transform: 'scale(1.3);' }, + '28%': { transform: 'scale(1);' }, + '42%': { transform: 'scale(1.3);' }, + '70%': { transform: 'scale(1);' }, + }, + 'flip-horizontal': { + '50%': { transform: 'rotateY(180deg)' }, + }, + 'flip-vertical': { + '50%': { transform: 'rotateX(180deg)' }, + }, }, animation: { 'waving-hand': 'wave 2s linear infinite', @@ -108,6 +121,9 @@ export default { 'accordion-up': 'accordion-up 0.2s ease-out', 'collapsible-down': 'collapsible-down 0.2s ease-out', 'collapsible-up': 'collapsible-up 0.2s ease-out', + heartbeat: 'heartbeat 1s infinite', + hflip: 'flip-horizontal 2s infinite', + vflip: 'flip-certical 2s infinite', }, }, }, From 84eef8f9abadc758febf1d84e02fe7e2586a2878 Mon Sep 17 00:00:00 2001 From: flipvanhaaren Date: Wed, 9 Oct 2024 13:46:50 +0200 Subject: [PATCH 14/28] dialog/sheet tuning --- frontend/src/modules/common/dialoger/dialog.tsx | 13 ++++++++----- frontend/src/modules/common/dialoger/state.ts | 2 +- .../modules/organizations/members-table/index.tsx | 3 ++- frontend/src/modules/ui/form.tsx | 2 +- frontend/src/modules/ui/sheet.tsx | 3 ++- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/frontend/src/modules/common/dialoger/dialog.tsx b/frontend/src/modules/common/dialoger/dialog.tsx index 39920fcbb..e1bc8ef45 100644 --- a/frontend/src/modules/common/dialoger/dialog.tsx +++ b/frontend/src/modules/common/dialoger/dialog.tsx @@ -1,12 +1,13 @@ import { type DialogT, dialog as dialogState } from '~/modules/common/dialoger/state'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '~/modules/ui/dialog'; +import { cn } from '~/utils/cn'; export interface DialogProp { dialog: DialogT; removeDialog: (dialog: DialogT) => void; } export default function StandardDialog({ dialog, removeDialog }: DialogProp) { - const { id, content, container, open, description, title, className, containerBackdrop, autoFocus, hideClose } = dialog; + const { id, content, container, open, description, title, className, containerBackdrop, containerBackdropClassName, autoFocus, hideClose } = dialog; const closeDialog = () => { removeDialog(dialog); @@ -20,13 +21,15 @@ export default function StandardDialog({ dialog, removeDialog }: DialogProp) { return ( {container && containerBackdrop && ( -
    +
    )} { - if (container && !containerBackdrop) e.preventDefault(); - }} hideClose={hideClose} onOpenAutoFocus={(event: Event) => { if (!autoFocus) event.preventDefault(); diff --git a/frontend/src/modules/common/dialoger/state.ts b/frontend/src/modules/common/dialoger/state.ts index 45f90d1cd..4b3eb9e6d 100644 --- a/frontend/src/modules/common/dialoger/state.ts +++ b/frontend/src/modules/common/dialoger/state.ts @@ -11,6 +11,7 @@ export type DialogT = { className?: string; refocus?: boolean; containerBackdrop?: boolean; + containerBackdropClassName?: string; autoFocus?: boolean; hideClose?: boolean; content?: React.ReactNode; @@ -117,7 +118,6 @@ const dialogFunction = (content: React.ReactNode, data?: ExternalDialog) => { DialogState.set({ content, drawerOnMobile: true, - containerBackdrop: true, refocus: true, autoFocus: true, hideClose: false, diff --git a/frontend/src/modules/organizations/members-table/index.tsx b/frontend/src/modules/organizations/members-table/index.tsx index 3f1af0377..56d20ee7b 100644 --- a/frontend/src/modules/organizations/members-table/index.tsx +++ b/frontend/src/modules/organizations/members-table/index.tsx @@ -178,7 +178,8 @@ const MembersTable = ({ entity, isSheet = false }: MembersTableProps) => { drawerOnMobile: false, className: 'w-auto shadow-none relative z-[60] max-w-4xl', container: containerRef.current, - containerBackdrop: false, + containerBackdrop: true, + containerBackdropClassName: 'z-50', title: t('common:invite'), description: `${t('common:invite_users.text')}`, }); diff --git a/frontend/src/modules/ui/form.tsx b/frontend/src/modules/ui/form.tsx index e0c0652bb..a8b606914 100644 --- a/frontend/src/modules/ui/form.tsx +++ b/frontend/src/modules/ui/form.tsx @@ -161,7 +161,7 @@ const FormDescription = React.forwardRef} {!collapsed && } - {!collapsed && {children}} + {!collapsed && {children}}
    ); diff --git a/frontend/src/modules/ui/sheet.tsx b/frontend/src/modules/ui/sheet.tsx index bf91529d3..7a38e7bc9 100644 --- a/frontend/src/modules/ui/sheet.tsx +++ b/frontend/src/modules/ui/sheet.tsx @@ -28,8 +28,9 @@ const SheetOverlay = React.forwardRef Date: Wed, 9 Oct 2024 14:41:29 +0200 Subject: [PATCH 15/28] handle ESC and click outside for sheet with nav sheet in mind --- frontend/src/modules/common/sheeter/sheet.tsx | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/frontend/src/modules/common/sheeter/sheet.tsx b/frontend/src/modules/common/sheeter/sheet.tsx index 5e4e80478..ba07a5a4e 100644 --- a/frontend/src/modules/common/sheeter/sheet.tsx +++ b/frontend/src/modules/common/sheeter/sheet.tsx @@ -1,3 +1,4 @@ +import { useRef } from 'react'; import StickyBox from '~/modules/common/sticky-box'; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '~/modules/ui/sheet'; import { type SheetT, sheet as sheetState } from './state'; @@ -8,6 +9,8 @@ export interface SheetProp { export default function DesktopSheet({ sheet, removeSheet }: SheetProp) { const { id, modal = true, side = 'right', open, description, title, hideClose = true, className, content } = sheet; + const sheetRef = useRef(null); + const closeSheet = () => { removeSheet(sheet); sheet.removeCallback?.(); @@ -19,10 +22,28 @@ export default function DesktopSheet({ sheet, removeSheet }: SheetProp) { if (!open) closeSheet(); }; + const handleEscapeKeyDown = (e: KeyboardEvent) => { + const activeElement = document.activeElement; + // Don't close sheet if not in modal and active element is not in sheet + if (!modal && !sheetRef.current?.contains(activeElement)) return; + e.preventDefault(); + closeSheet(); + }; + + // Close sheet if clicked outside and not in modal + const handleInteractOutside = (event: CustomEvent<{ originalEvent: PointerEvent }> | CustomEvent<{ originalEvent: FocusEvent }>) => { + const mainContentElement = document.getElementById('main-block-app-content'); + if (!modal && mainContentElement?.contains(event.target as Node)) { + return closeSheet(); + } + }; + return ( Date: Wed, 9 Oct 2024 16:20:45 +0200 Subject: [PATCH 16/28] nav fixes --- frontend/src/hooks/use-breakpoints.tsx | 10 ++++- frontend/src/modules/common/main-app.tsx | 2 + frontend/src/modules/common/main-content.tsx | 2 +- .../modules/common/main-nav/bar-nav/index.tsx | 9 +---- .../src/modules/common/main-nav/index.tsx | 21 ++++++---- .../modules/common/nav-sheet/sheet-menu.tsx | 10 ++++- frontend/src/modules/common/set-body.tsx | 40 +++++++++++++++++++ .../src/modules/common/sheeter/drawer.tsx | 2 +- frontend/src/modules/common/sheeter/index.tsx | 1 + frontend/src/modules/common/sheeter/sheet.tsx | 5 ++- frontend/src/store/navigation.ts | 29 +++++++++++--- 11 files changed, 104 insertions(+), 27 deletions(-) create mode 100644 frontend/src/modules/common/set-body.tsx diff --git a/frontend/src/hooks/use-breakpoints.tsx b/frontend/src/hooks/use-breakpoints.tsx index 04b21ccb3..cc1a9502f 100644 --- a/frontend/src/hooks/use-breakpoints.tsx +++ b/frontend/src/hooks/use-breakpoints.tsx @@ -3,7 +3,11 @@ import { useEffect, useState } from 'react'; type ValidBreakpoints = keyof typeof config.theme.screenSizes; -export const useBreakpoints = (mustBe: 'min' | 'max', breakpoint: ValidBreakpoints): boolean => { +export const useBreakpoints = ( + mustBe: 'min' | 'max', + breakpoint: ValidBreakpoints, + enableReactivity = true, // Optional parameter to enable/disable reactivity +) => { const breakpoints: { [key: string]: string } = config.theme.screenSizes; // Sort breakpoints by their pixel value in ascending order @@ -39,6 +43,8 @@ export const useBreakpoints = (mustBe: 'min' | 'max', breakpoint: ValidBreakpoin // Update breakpoints on window resize useEffect(() => { + if (!enableReactivity) return; + const updateBreakpoints = () => { setCurrentBreakpoints(getMatchedBreakpoints()); }; @@ -51,7 +57,7 @@ export const useBreakpoints = (mustBe: 'min' | 'max', breakpoint: ValidBreakpoin // Cleanup on unmount return () => window.removeEventListener('resize', updateBreakpoints); - }, [breakpoints]); + }, [breakpoints, enableReactivity]); // Get the index of the current largest matched breakpoint and target breakpoint const currentBreakpointIndex = sortedBreakpoints.indexOf(currentBreakpoints[currentBreakpoints.length - 1]); diff --git a/frontend/src/modules/common/main-app.tsx b/frontend/src/modules/common/main-app.tsx index e09984819..b4ceaaf67 100644 --- a/frontend/src/modules/common/main-app.tsx +++ b/frontend/src/modules/common/main-app.tsx @@ -5,6 +5,7 @@ import { Dialoger } from '~/modules/common/dialoger'; import { DropDowner } from '~/modules/common/dropdowner'; import ErrorNotice from '~/modules/common/error-notice'; import MainNav from '~/modules/common/main-nav'; +import { SetBody } from '~/modules/common/set-body'; import { Sheeter } from '~/modules/common/sheeter'; import SSE from '~/modules/common/sse'; import { SSEProvider } from '~/modules/common/sse/provider'; @@ -17,6 +18,7 @@ const App = () => { > + diff --git a/frontend/src/modules/common/main-content.tsx b/frontend/src/modules/common/main-content.tsx index c0d0f8e17..85419abb0 100644 --- a/frontend/src/modules/common/main-content.tsx +++ b/frontend/src/modules/common/main-content.tsx @@ -5,7 +5,7 @@ import AlertRenderer from '~/modules/common/main-alert/alert-render'; export const MainContent = () => { return (
    -
    +
    diff --git a/frontend/src/modules/common/main-nav/bar-nav/index.tsx b/frontend/src/modules/common/main-nav/bar-nav/index.tsx index acfa15f7b..2e7954c1a 100644 --- a/frontend/src/modules/common/main-nav/bar-nav/index.tsx +++ b/frontend/src/modules/common/main-nav/bar-nav/index.tsx @@ -5,7 +5,6 @@ import { Fragment, Suspense, lazy, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { toast } from 'sonner'; import { impersonationStop } from '~/api/auth'; -import useBodyClass from '~/hooks/use-body-class'; import useMounted from '~/hooks/use-mounted'; import type { NavItem } from '~/modules/common/main-nav'; import { NavButton } from '~/modules/common/main-nav/bar-nav/bar-nav-button'; @@ -24,13 +23,10 @@ const BarNav = ({ items, onClick }: { items: NavItem[]; onClick: (index: number) const { user } = useUserStore(); const { theme } = useThemeStore(); - const { focusView, keepMenuOpen, navSheetOpen } = useNavigationStore(); + const { navSheetOpen } = useNavigationStore(); const currentSession = useMemo(() => user?.sessions.find((s) => s.isCurrent), [user]); - // Keep menu open - useBodyClass({ 'keep-nav-open': keepMenuOpen, 'nav-open': !!navSheetOpen }); - const stopImpersonation = async () => { await impersonationStop(); await Promise.all([getAndSetMe(), getAndSetMenu()]); @@ -43,10 +39,9 @@ const BarNav = ({ items, onClick }: { items: NavItem[]; onClick: (index: number)