From 0a34db8989f39c119cee64ce3406666b16fddd31 Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Thu, 17 Jul 2025 21:58:09 -0600 Subject: [PATCH] revise front end to use shared components --- apps/api/turbo.json | 6 +- .../features/ShareMenu/AccessDropdown.tsx | 78 +++++++++--- .../ShareMenu/IndividualSharePerson.tsx | 37 ------ .../ShareMenu/ShareMenuContentBody.tsx | 55 ++++++--- .../features/ShareMenu/ShareMenuInvite.tsx | 1 + .../features/ShareMenu/ShareRowItem.tsx | 42 +++++++ .../features/ShareMenu/WorkspaceAvatar.tsx | 17 +++ .../ShareMenu/WorkspaceShareSection.tsx | 114 ------------------ .../components/ui/avatar/AvatarUserButton.tsx | 8 +- .../Supabase/SupabaseContextProvider.tsx | 2 +- .../src/share/share-interfaces.types.ts | 9 +- 11 files changed, 173 insertions(+), 196 deletions(-) delete mode 100644 apps/web/src/components/features/ShareMenu/IndividualSharePerson.tsx create mode 100644 apps/web/src/components/features/ShareMenu/ShareRowItem.tsx create mode 100644 apps/web/src/components/features/ShareMenu/WorkspaceAvatar.tsx delete mode 100644 apps/web/src/components/features/ShareMenu/WorkspaceShareSection.tsx diff --git a/apps/api/turbo.json b/apps/api/turbo.json index 7751b1901..ff9c7a825 100644 --- a/apps/api/turbo.json +++ b/apps/api/turbo.json @@ -2,6 +2,10 @@ "$schema": "https://turbo.build/schema.json", "extends": ["//"], "tasks": { - "dev": {} + "dev": { + "dependsOn": ["@buster/database#start"], + "with": [], + "outputs": [] + } } } diff --git a/apps/web/src/components/features/ShareMenu/AccessDropdown.tsx b/apps/web/src/components/features/ShareMenu/AccessDropdown.tsx index a0f0c7fd4..afae9f714 100644 --- a/apps/web/src/components/features/ShareMenu/AccessDropdown.tsx +++ b/apps/web/src/components/features/ShareMenu/AccessDropdown.tsx @@ -1,5 +1,5 @@ import React, { useMemo } from 'react'; -import type { ShareAssetType, ShareRole } from '@buster/server-shared/share'; +import type { ShareAssetType, ShareRole, WorkspaceShareRole } from '@buster/server-shared/share'; import type { DropdownItem } from '@/components/ui/dropdown'; import { Dropdown } from '@/components/ui/dropdown'; import { ChevronDown } from '@/components/ui/icons/NucleoIconFilled'; @@ -7,24 +7,45 @@ import { Paragraph, Text } from '@/components/ui/typography'; import { useMemoizedFn } from '@/hooks'; import { cn } from '@/lib/classMerge'; -type DropdownValue = ShareRole | 'remove' | 'notShared'; +type DropdownValue = ShareRole | WorkspaceShareRole | 'remove'; -export const AccessDropdown: React.FC<{ - className?: string; - showRemove?: boolean; - shareLevel?: ShareRole | null; - onChangeShareLevel?: (level: ShareRole | null) => void; +type AccessDropdownBaseProps = { assetType: ShareAssetType; disabled: boolean; -}> = ({ + className?: string; +}; + +type AccessDropdownUserProps = { + showRemove?: boolean; + shareLevel?: ShareRole | null; + type: 'user'; + onChangeShareLevel?: (level: ShareRole | null) => void; +}; + +type AccessDropdownWorkspaceProps = { + type: 'workspace'; + shareLevel?: WorkspaceShareRole | null; + onChangeShareLevel?: (level: WorkspaceShareRole | null) => void; +}; + +type AccessDropdownProps = AccessDropdownBaseProps & + (AccessDropdownUserProps | AccessDropdownWorkspaceProps); + +export const AccessDropdown: React.FC = ({ shareLevel, - showRemove = true, disabled, className = '', onChangeShareLevel, - assetType + assetType, + ...props }) => { + const showRemove = props.type === 'user' && props.showRemove !== false; + const items = useMemo(() => { + if (props.type === 'workspace') { + return workspaceItems; + } + const baseItems: DropdownItem[] = [...(itemsRecord[assetType] || [])]; if (showRemove) { @@ -38,7 +59,7 @@ export const AccessDropdown: React.FC<{ ...item, selected: item.value === shareLevel })); - }, [showRemove, shareLevel, assetType]); + }, [showRemove, shareLevel, assetType, props.type]); const selectedLabel = useMemo(() => { const selectedItem = items.find((item) => item.selected) || OWNER_ITEM; @@ -51,16 +72,17 @@ export const AccessDropdown: React.FC<{ return 'Full access'; case 'canEdit': return 'Can edit'; - case 'canFilter': - return 'Can filter'; case 'canView': return 'Can view'; case 'owner': return 'Owner'; case 'remove': return 'Remove'; - case 'notShared': + case 'none': return 'Not shared'; + default: + const _exhaustiveCheck: never = value; + return value; } }, [items]); @@ -128,11 +150,6 @@ const dashboardItems: DropdownItem[] = [ label: 'Can edit', secondaryLabel: 'Can edit but not share with others.' }, - // { - // value: 'canFilter', - // label: 'Can filter', - // secondaryLabel: 'Can filter dashboards but not edit.' - // }, { value: 'canView', label: 'Can view', @@ -158,6 +175,29 @@ const collectionItems: DropdownItem[] = [ } ]; +const workspaceItems: DropdownItem[] = [ + { + value: 'fullAccess', + label: 'Full access', + secondaryLabel: 'Can edit and share with others.' + }, + { + value: 'canEdit', + label: 'Can edit', + secondaryLabel: 'Can edit, but not share with others.' + }, + { + value: 'canView', + label: 'Can view', + secondaryLabel: 'Cannot edit or share with others.' + }, + { + value: 'none', + label: 'Not shared', + secondaryLabel: 'Does not have access.' + } +]; + const itemsRecord: Record[]> = { ['dashboard']: dashboardItems, ['metric']: metricItems, diff --git a/apps/web/src/components/features/ShareMenu/IndividualSharePerson.tsx b/apps/web/src/components/features/ShareMenu/IndividualSharePerson.tsx deleted file mode 100644 index ac1ede932..000000000 --- a/apps/web/src/components/features/ShareMenu/IndividualSharePerson.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import { useMemoizedFn } from '@/hooks'; -import { AccessDropdown } from './AccessDropdown'; -import type { ShareAssetType, ShareRole } from '@buster/server-shared/share'; -import { AvatarUserButton } from '../../ui/avatar/AvatarUserButton'; - -export const IndividualSharePerson: React.FC<{ - name?: string; - email: string; - role: ShareRole; - avatar_url?: string | null; - onUpdateShareRole: (email: string, role: ShareRole | null) => void; - assetType: ShareAssetType; - disabled: boolean; -}> = React.memo(({ name, onUpdateShareRole, email, avatar_url, role, assetType, disabled }) => { - const onChangeShareLevel = useMemoizedFn((v: ShareRole | null) => { - onUpdateShareRole(email, v); - }); - - return ( -
- - - -
- ); -}); - -IndividualSharePerson.displayName = 'IndividualSharePerson'; diff --git a/apps/web/src/components/features/ShareMenu/ShareMenuContentBody.tsx b/apps/web/src/components/features/ShareMenu/ShareMenuContentBody.tsx index aac08437b..dab4eadd3 100644 --- a/apps/web/src/components/features/ShareMenu/ShareMenuContentBody.tsx +++ b/apps/web/src/components/features/ShareMenu/ShareMenuContentBody.tsx @@ -1,16 +1,23 @@ -import React from 'react'; -import type { ShareAssetType, ShareConfig, ShareRole, WorkspaceShareRole } from '@buster/server-shared/share'; +import React, { useMemo } from 'react'; +import type { + ShareAssetType, + ShareConfig, + ShareRole, + WorkspaceShareRole +} from '@buster/server-shared/share'; import { useUnshareCollection, useUpdateCollectionShare } from '@/api/buster_rest/collections'; import { useUnshareDashboard, useUpdateDashboardShare } from '@/api/buster_rest/dashboards'; import { useUnshareMetric, useUpdateMetricShare } from '@/api/buster_rest/metrics'; import { useMemoizedFn } from '@/hooks'; import { cn } from '@/lib/classMerge'; -import { IndividualSharePerson } from './IndividualSharePerson'; import { ShareMenuContentEmbed } from './ShareMenuContentEmbed'; import { ShareMenuContentPublish } from './ShareMenuContentPublish'; import type { ShareMenuTopBarOptions } from './ShareMenuTopBar'; import { ShareMenuInvite } from './ShareMenuInvite'; import { WorkspaceShareSection } from './WorkspaceShareSection'; +import { ShareRowItem } from './ShareRowItem'; +import { WorkspaceAvatar } from './WorkspaceAvatar'; +import pluralize from 'pluralize'; export const ShareMenuContentBody: React.FC<{ selectedOptions: ShareMenuTopBarOptions; @@ -55,7 +62,14 @@ export const ShareMenuContentBody: React.FC<{ ShareMenuContentBody.displayName = 'ShareMenuContentBody'; const ShareMenuContentShare: React.FC = React.memo( - ({ canEditPermissions, assetType, individual_permissions, assetId, className, shareAssetConfig }) => { + ({ + canEditPermissions, + assetType, + individual_permissions, + assetId, + className, + shareAssetConfig + }) => { const { mutateAsync: onUpdateMetricShare } = useUpdateMetricShare(); const { mutateAsync: onUpdateDashboardShare } = useUpdateDashboardShare(); const { mutateAsync: onUpdateCollectionShare } = useUpdateCollectionShare(); @@ -64,6 +78,7 @@ const ShareMenuContentShare: React.FC = React.memo( const { mutateAsync: onUnshareCollection } = useUnshareCollection(); const hasIndividualPermissions = !!individual_permissions?.length; + const workspaceMemberCount = shareAssetConfig.workspace_member_count || 0; const onUpdateShareRole = useMemoizedFn(async (email: string, role: ShareRole | null) => { if (role) { @@ -107,7 +122,7 @@ const ShareMenuContentShare: React.FC = React.memo( workspace_sharing: role } }; - + if (assetType === 'metric') { await onUpdateMetricShare(payload); } else if (assetType === 'dashboard') { @@ -130,24 +145,36 @@ const ShareMenuContentShare: React.FC = React.memo( {hasIndividualPermissions && (
{individual_permissions?.map((permission) => ( - + onUpdateShareRole(permission.email, role) + )} assetType={assetType} disabled={!canEditPermissions || permission.role === 'owner'} + type="user" /> ))}
)} {canEditPermissions && ( - ( + + ), + [] + )} + onChangeShareLevel={onUpdateWorkspacePermissions} assetType={assetType} - assetId={assetId} - canEditPermissions={canEditPermissions} - onUpdateWorkspacePermissions={onUpdateWorkspacePermissions} /> )} diff --git a/apps/web/src/components/features/ShareMenu/ShareMenuInvite.tsx b/apps/web/src/components/features/ShareMenu/ShareMenuInvite.tsx index 5692d2d91..8ab121100 100644 --- a/apps/web/src/components/features/ShareMenu/ShareMenuInvite.tsx +++ b/apps/web/src/components/features/ShareMenu/ShareMenuInvite.tsx @@ -143,6 +143,7 @@ export const ShareMenuInvite: React.FC = React.memo( onChangeShareLevel={onChangeAccessDropdown} assetType={assetType} disabled={false} + type="user" /> )} diff --git a/apps/web/src/components/features/ShareMenu/ShareRowItem.tsx b/apps/web/src/components/features/ShareMenu/ShareRowItem.tsx new file mode 100644 index 000000000..906c321c7 --- /dev/null +++ b/apps/web/src/components/features/ShareMenu/ShareRowItem.tsx @@ -0,0 +1,42 @@ +import type { ShareAssetType, ShareRole, WorkspaceShareRole } from '@buster/server-shared/share'; +import React from 'react'; +import { AvatarUserButton } from '../../ui/avatar/AvatarUserButton'; +import { AccessDropdown } from './AccessDropdown'; + +type ShareRowItemBaseProps = { + primary: string | undefined; + secondary: string | undefined; + avatar?: string | null | React.ReactNode; + disabled?: boolean; + assetType: ShareAssetType; +}; + +type ShareRowItemUserProps = { + type: 'user'; + role: ShareRole; + showRemove?: boolean; + onChangeShareLevel?: (role: ShareRole | null) => void; +}; + +type ShareRowItemWorkspaceProps = { + type: 'workspace'; + role: WorkspaceShareRole; + onChangeShareLevel?: (role: WorkspaceShareRole | null) => void; +}; + +type ShareRowItemProps = ShareRowItemBaseProps & + (ShareRowItemUserProps | ShareRowItemWorkspaceProps); + +export const ShareRowItem: React.FC = React.memo( + ({ primary, secondary, avatar, disabled = false, assetType, ...props }) => { + return ( +
+ + + +
+ ); + } +); diff --git a/apps/web/src/components/features/ShareMenu/WorkspaceAvatar.tsx b/apps/web/src/components/features/ShareMenu/WorkspaceAvatar.tsx new file mode 100644 index 000000000..c4308f200 --- /dev/null +++ b/apps/web/src/components/features/ShareMenu/WorkspaceAvatar.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { ApartmentBuilding } from '@/components/ui/icons'; +import { cn } from '@/lib/classMerge'; + +export const WorkspaceAvatar: React.FC<{ + className?: string; +}> = React.memo(({ className }) => { + return ( +
+ +
+ ); +}); diff --git a/apps/web/src/components/features/ShareMenu/WorkspaceShareSection.tsx b/apps/web/src/components/features/ShareMenu/WorkspaceShareSection.tsx deleted file mode 100644 index a433aa4d3..000000000 --- a/apps/web/src/components/features/ShareMenu/WorkspaceShareSection.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import React from 'react'; -import type { ShareAssetType, ShareConfig, WorkspaceShareRole } from '@buster/server-shared/share'; -import { Dropdown } from '@/components/ui/dropdown'; -import type { DropdownItem } from '@/components/ui/dropdown'; -import { ChevronDown } from '@/components/ui/icons/NucleoIconFilled'; -import { ApartmentBuilding } from '@/components/ui/icons/NucleoIconFilled'; -import { Text } from '@/components/ui/typography'; -import { useMemoizedFn } from '@/hooks'; -import { cn } from '@/lib/classMerge'; - -const workspaceShareRoleItems: DropdownItem[] = [ - { - value: 'fullAccess', - label: 'Full access', - secondaryLabel: 'Can edit and share with others.' - }, - { - value: 'canEdit', - label: 'Can edit', - secondaryLabel: 'Can edit, but not share with others.' - }, - { - value: 'canView', - label: 'Can view', - secondaryLabel: 'Cannot edit or share with others.' - }, - { - value: 'none', - label: 'Not shared', - secondaryLabel: 'Does not have access.' - } -]; - -interface WorkspaceShareSectionProps { - shareAssetConfig: ShareConfig; - assetType: ShareAssetType; - assetId: string; - canEditPermissions: boolean; - onUpdateWorkspacePermissions: (role: WorkspaceShareRole | null) => void; -} - -export const WorkspaceShareSection: React.FC = React.memo(({ - shareAssetConfig, - canEditPermissions, - onUpdateWorkspacePermissions -}) => { - const currentRole = shareAssetConfig.workspace_sharing || 'none'; - - const selectedLabel = React.useMemo(() => { - const selectedItem = workspaceShareRoleItems.find(item => item.value === currentRole); - return selectedItem?.label || 'Not shared'; - }, [currentRole]); - - const onSelectMenuItem = useMemoizedFn((value: string) => { - onUpdateWorkspacePermissions(value as WorkspaceShareRole); - }); - - const items = React.useMemo(() => { - return workspaceShareRoleItems.map(item => ({ - ...item, - selected: item.value === currentRole - })); - }, [currentRole]); - - return ( -
-
-
- -
-
- Workspace - - Share with {shareAssetConfig.workspace_member_count?.toLocaleString() || 0} members - -
-
- - - - Sharing cannot override permissions set by your account admins. - -
- } - footerClassName="p-0!" - onSelect={onSelectMenuItem} - sideOffset={16} - selectType="single" - align="end" - side="bottom"> - - {selectedLabel} - {canEditPermissions && ( - - - - )} - - - - ); -}); - -WorkspaceShareSection.displayName = 'WorkspaceShareSection'; \ No newline at end of file diff --git a/apps/web/src/components/ui/avatar/AvatarUserButton.tsx b/apps/web/src/components/ui/avatar/AvatarUserButton.tsx index aa7dcb0b0..7e610549f 100644 --- a/apps/web/src/components/ui/avatar/AvatarUserButton.tsx +++ b/apps/web/src/components/ui/avatar/AvatarUserButton.tsx @@ -7,7 +7,7 @@ export const AvatarUserButton = React.forwardRef< HTMLDivElement, { username?: string | null; - avatarUrl?: string | null; + avatarUrl?: string | null | React.ReactNode; email?: string | null; className?: string; avatarSize?: number; @@ -17,7 +17,11 @@ export const AvatarUserButton = React.forwardRef< return (
- + {typeof avatarUrl === 'string' || avatarUrl === null ? ( + + ) : ( + avatarUrl + )}
{username} diff --git a/apps/web/src/context/Supabase/SupabaseContextProvider.tsx b/apps/web/src/context/Supabase/SupabaseContextProvider.tsx index 9f04029c8..8797a9107 100644 --- a/apps/web/src/context/Supabase/SupabaseContextProvider.tsx +++ b/apps/web/src/context/Supabase/SupabaseContextProvider.tsx @@ -88,7 +88,7 @@ const useSupabaseContextInternal = ({ async ({ accessToken, expiresAt: _expiresAt }: { accessToken: string; expiresAt: number }) => { setAccessToken(accessToken); flushSync(() => { - openInfoMessage('Token refreshed'); + //noop }); } ); diff --git a/packages/server-shared/src/share/share-interfaces.types.ts b/packages/server-shared/src/share/share-interfaces.types.ts index c7abfd53b..9fed87fd6 100644 --- a/packages/server-shared/src/share/share-interfaces.types.ts +++ b/packages/server-shared/src/share/share-interfaces.types.ts @@ -4,17 +4,10 @@ export const ShareRoleSchema = z.enum([ 'owner', //owner of the asset 'fullAccess', //same as owner, can share with others 'canEdit', //can edit, cannot share - 'canFilter', //can filter dashboard 'canView', //can view asset ]); -export const WorkspaceShareRoleSchema = z.enum([ - 'owner', - 'fullAccess', - 'canEdit', - 'canView', - 'none', -]); +export const WorkspaceShareRoleSchema = z.enum([...ShareRoleSchema.options, 'none']); export const ShareAssetTypeSchema = z.enum(['metric', 'dashboard', 'collection', 'chat']);