Skip to content

Commit

Permalink
Merge pull request #691 from contember/feat/dimensions
Browse files Browse the repository at this point in the history
dimensions switcher and renderer
  • Loading branch information
matej21 authored Apr 22, 2024
2 parents 7e4f3cd + 0053696 commit bcf74f3
Show file tree
Hide file tree
Showing 26 changed files with 765 additions and 20 deletions.
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

0 comments on commit bcf74f3

Please sign in to comment.