Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dimensions switcher and renderer #691

Merged
merged 6 commits into from
Apr 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions build/api/admin.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -3776,9 +3776,6 @@ identityId: GQLVariableType<string, true>;
memberships: GQLVariableType<Membership[], true>;
}>, TenantMutationResponse<never, UpdateMembershipErrorCodes>>;

// @public (undocumented)
export const Variable: React.MemoExoticComponent<({ name, format }: VariableProps) => ReactElement>;

// @public (undocumented)
export interface VariableConfig {
// (undocumented)
Expand All @@ -3788,14 +3785,6 @@ export interface VariableConfig {
}>;
}

// @public (undocumented)
export interface VariableProps {
// (undocumented)
format?: (value: ReactNode) => ReactNode;
// (undocumented)
name: Environment.Name;
}

// @public (undocumented)
export interface Variables {
// (undocumented)
Expand Down
23 changes: 23 additions & 0 deletions build/api/react-binding.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ import type { HasManyRelationMarker } from '@contember/binding';
import type { HasOneRelationMarker } from '@contember/binding';
import { JSX as JSX_2 } from 'react/jsx-runtime';
import { MarkerTreeRoot } from '@contember/binding';
import { MemoExoticComponent } from 'react';
import { NamedExoticComponent } from 'react';
import type { Persist } from '@contember/binding';
import { PropsWithChildren } from 'react';
import * as React_2 from 'react';
import { ReactElement } from 'react';
import { ReactNode } from 'react';
import type { RelativeEntityList } from '@contember/binding';
Expand Down Expand Up @@ -169,6 +171,16 @@ export interface DeferredSubTreesProps {
fallback: ReactNode;
}

// @public (undocumented)
export const DimensionRenderer: React_2.NamedExoticComponent<DimensionRendererProps>;

// @public (undocumented)
export type DimensionRendererProps = {
dimension: string;
as: string;
children: ReactNode;
};

// @public (undocumented)
export const DirtinessContext: Context<boolean>;

Expand Down Expand Up @@ -766,6 +778,17 @@ export const useSortedEntities: (entityList: EntityListAccessor, sortableByField
// @public (undocumented)
export const useTreeRootId: () => TreeRootId | undefined;

// @public (undocumented)
export const Variable: MemoExoticComponent<({ name, format }: VariableProps) => ReactElement>;

// @public (undocumented)
export interface VariableProps {
// (undocumented)
format?: (value: ReactNode) => ReactNode;
// (undocumented)
name: Environment.Name;
}


export * from "@contember/binding";

Expand Down
26 changes: 26 additions & 0 deletions build/api/react-routing.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,32 @@ import { JSX as JSX_2 } from 'react/jsx-runtime';
import { NamedExoticComponent } from 'react';
import { ReactElement } from 'react';
import { ReactNode } from 'react';
import { StateStorageOrName } from '@contember/react-utils';

// @public (undocumented)
export const createBindingLinkParametersResolver: (entity: EntityAccessor | undefined) => RoutingParameterResolver;

// @public (undocumented)
export const CurrentRequestContext: Context<RequestState>;

// @public (undocumented)
export const DimensionLink: NamedExoticComponent<DimensionLinkProps>;

// @public (undocumented)
export type DimensionLinkAction = 'add' | 'toggle' | 'set' | 'unset';

// @public (undocumented)
export interface DimensionLinkProps {
// (undocumented)
action?: DimensionLinkAction;
// (undocumented)
children: ReactElement;
// (undocumented)
dimension: string;
// (undocumented)
value: string;
}

// @public (undocumented)
export type DynamicRequestParameters = RequestParameters<RoutingParameter>;

Expand Down Expand Up @@ -269,6 +288,13 @@ export const useBindingLinkParametersResolver: () => RoutingParameterResolver;
// @public (undocumented)
export const useCurrentRequest: () => RequestState;

// @public (undocumented)
export const useDimensionState: ({ dimension, defaultValue, storage }: {
dimension: string;
defaultValue: string | string[];
storage?: StateStorageOrName | undefined;
}) => string[];

// @public (undocumented)
export const useLinkFactory: () => (target: RoutingLinkTarget, parameters?: RequestParameters, entity?: EntityAccessor) => RoutingLinkParams;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export * from './SideDimensions'
export * from './DimensionsSwitcher'
export * from './Variable'
1 change: 1 addition & 0 deletions packages/playground/admin/app/components/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as React from 'react'
import { memo, PropsWithChildren } from 'react'
import { IdentityLoader } from '../../lib/components/binding/identity'
import { Slots } from '../../lib/components/slots'
Expand Down
3 changes: 2 additions & 1 deletion packages/playground/admin/app/components/navigation.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ArchiveIcon, BrushIcon, FormInputIcon, GripVertical, HomeIcon, KanbanIcon, TableIcon, UploadIcon } from 'lucide-react'
import { ArchiveIcon, BrushIcon, FormInputIcon, GripVertical, HomeIcon, KanbanIcon, LanguagesIcon, TableIcon, UploadIcon } from 'lucide-react'
import { Menu, MenuItem, MenuList } from '../../lib/components/ui/menu'


Expand Down Expand Up @@ -35,6 +35,7 @@ export const Navigation = () => {
<MenuItem icon={line} label={'Has many select'} to={'select/hasMany'} />
<MenuItem icon={line} label={'Has many sortable select'} to={'select/hasManySortable'} />
</MenuItem>
<MenuItem icon={<LanguagesIcon size={16} />} label={'Dimensions'} to={'dimensions'} />
</Menu>
</div>
)
Expand Down
39 changes: 39 additions & 0 deletions packages/playground/admin/app/pages/dimensions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Slots } from '../../lib/components/slots'
import { Binding, PersistButton } from '../../lib/components/binding'
import * as React from 'react'
import { DimensionsSwitcher, SideDimensions } from '../../lib/components/dimensions'
import { EntitySubTree, Field, Variable } from '@contember/interface'
import { InputField, TextareaField } from '../../lib/components/form'
import { Card, CardContent, CardHeader, CardTitle } from '../../lib/components/ui/card'

export default () => {
return <>
<Binding>
<DimensionsSwitcher
options="DimensionsLocale"
slugField="code"
dimension="locale"
isMulti
>
<Field field="label" />
</DimensionsSwitcher>
</Binding>

<Binding>
<Slots.Actions><PersistButton /></Slots.Actions>
<EntitySubTree entity="DimensionsItem(unique=unique)">
<SideDimensions dimension="locale" as="currentLocale" field="locales(locale.code=$currentLocale)">
<Card>
<CardHeader>
<CardTitle><Variable name="currentLocale" /></CardTitle>
</CardHeader>
<CardContent>
<InputField field="title" />
<TextareaField field="content" />
</CardContent>
</Card>
</SideDimensions>
</EntitySubTree>
</Binding>
</>
}
2 changes: 0 additions & 2 deletions packages/playground/admin/app/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import * as React from 'react'

export default () => {
debugger
const foo = React.useMemo(() => 1, [])
return <>Hello!</>
}
135 changes: 135 additions & 0 deletions packages/playground/admin/lib/components/dimensions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import {
Component,
DimensionLink,
DimensionRenderer,
Entity,
EntityAccessor,
Field,
HasOne,
StaticRender,
SugaredQualifiedEntityList,
SugaredRelativeSingleEntity,
SugaredRelativeSingleField,
useDimensionState,
useEntity,
} from '@contember/interface'
import { DataView, DataViewEachRow, DataViewLoaderState, DataViewSortingDirections, useDataViewEntityListAccessor } from '@contember/react-dataview'
import * as React from 'react'
import { ReactNode, useMemo } from 'react'
import { CheckIcon } from 'lucide-react'
import { Loader } from './ui/loader'
import { Popover, PopoverContent, PopoverTrigger } from './ui/popover'
import { Button } from './ui/button'

export interface DimensionsSwitcherProps {
options: SugaredQualifiedEntityList['entities']
orderBy?: DataViewSortingDirections
dimension: string
children: ReactNode
slugField: SugaredRelativeSingleField['field']
isMulti?: boolean
}

export const DimensionsSwitcher = Component(({ options, dimension, children, slugField, orderBy, isMulti }: DimensionsSwitcherProps) => {
return (
<DataView entities={options} initialSorting={orderBy}>
<DataViewLoaderState initial refreshing>
<Loader position={'static'} />
</DataViewLoaderState>
<DataViewLoaderState loaded>

<Popover>
<PopoverTrigger>
<Button variant={'outline'} size="sm">
<DimensionSwitcherCurrentValues dimension={dimension} slugField={slugField}>
{children}
</DimensionSwitcherCurrentValues>
</Button>
</PopoverTrigger>
<PopoverContent className="p-2" align="start">
<div className="flex flex-col gap-1">
<DataViewEachRow>
<DimensionSwitcherItem dimension={dimension} slugField={slugField} isMulti={isMulti}>
{children}
<StaticRender>
<Field field={slugField} />
</StaticRender>
</DimensionSwitcherItem>
</DataViewEachRow>
</div>
</PopoverContent>
</Popover>
</DataViewLoaderState>
</DataView>
)
})

const DimensionSwitcherCurrentValues = ({ children, dimension, slugField }: { children: ReactNode, dimension: string, slugField: SugaredRelativeSingleField['field'] }) => {
const entitiesBySlug = useDimensionEntitiesBySlug(slugField)

const currentDimensionValue = useDimensionState({
dimension,
defaultValue: Object.keys(entitiesBySlug)[0],
storage: 'local',
})

const values = useMemo(() => currentDimensionValue.map(it => entitiesBySlug[it]).filter(Boolean), [currentDimensionValue, entitiesBySlug])

return (
<div className="flex gap-1">
{values.map(it => (
<Entity key={it.key} accessor={it}>
<div className={'gap-1 group text-black text-left inline-flex items-center px-1 text-sm border-b'}>
<span>{children}</span>
</div>
</Entity>
))}
</div>
)
}


const DimensionSwitcherItem = ({ children, dimension, slugField, isMulti }: { children: ReactNode, dimension: string, slugField: SugaredRelativeSingleField['field'], isMulti?: boolean }) => {
const entity = useEntity()
const slugValue = entity.getField<string>(slugField).value
if (!slugValue) {
return null
}

return (
<DimensionLink dimension={dimension} value={slugValue} action={isMulti ? 'toggle' : 'set'}>
<a className={'gap-1 group text-gray-800 text-left inline-flex items-center px-1 py-1 text-sm rounded transition-all hover:bg-accent hover:text-accent-foreground group data-[active]:text-black'}>
<CheckIcon className={'w-3 h-3 hidden group-data-[active]:block'} />
<span className={'w-3 h-3 group-data-[active]:hidden'} />
<span>{children}</span>
</a>
</DimensionLink>
)
}

export interface SideDimensionsProps {
dimension: string
as: string
field: SugaredRelativeSingleEntity['field']
children: ReactNode
}

export const SideDimensions = Component<SideDimensionsProps>(({ dimension, children, as, field }) => {
return (
<div className="flex mt-4 gap-4">
<DimensionRenderer dimension={dimension} as={as}>
<HasOne field={field}>
<div className="flex-1">
{children}
</div>
</HasOne>
</DimensionRenderer>
</div>
)
})


const useDimensionEntitiesBySlug = (slugField: SugaredRelativeSingleField['field']): Record<string, EntityAccessor> => {
const accessor = useDataViewEntityListAccessor()
return useMemo(() => Object.fromEntries(Array.from(accessor ?? []).map(it => [it.getField<string>(slugField).value, it])), [accessor, slugField])
}
Loading
Loading