Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
pnicolli committed Feb 20, 2024
1 parent 0dff987 commit 905c0fe
Show file tree
Hide file tree
Showing 59 changed files with 8,608 additions and 177 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,13 @@
"@eeacms/volto-taxonomy": "^1.0.0",
"@loadable/babel-plugin": "5.13.2",
"@plone-collective/volto-sentry": "0.3.0",
"@plone/components": "alpha",
"bootstrap-italia": "2.6.1",
"classnames": "^2.3.2",
"design-react-kit": "5.0.0-1",
"htmldiff-js": "1.0.5",
"marked": "9.0.0",
"react-aria-components": "1.1.1",
"react-dropzone": "11.0.1",
"react-focus-lock": "2.9.4",
"react-google-recaptcha-v3": "1.7.0",
Expand Down Expand Up @@ -166,6 +168,7 @@
"@commitlint/cli": "17.6.6",
"@commitlint/config-conventional": "17.6.6",
"@plone/scripts": "*",
"@plone/types": "1.0.0-alpha.3",
"@release-it/conventional-changelog": "5.1.1",
"eslint": "8.54.0",
"eslint-config-prettier": "9.0.0",
Expand Down
29 changes: 29 additions & 0 deletions src/components/Contents/AddContentPopover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react';
import { Link, ChevronrightIcon, Popover } from '@plone/components';

interface Props {
addableTypes: {
'@id': string;
id: string;
title: string;
}[];
}

export const AddContentPopover = ({ addableTypes }: Props) => {
// const page = addableTypes.find((type) => type.id === 'Document');

return (
<Popover className="react-aria-Popover add-content-popover">
<ul className="add-content-list">
{addableTypes.map((type) => (
<li key={type.id} className="add-content-list-item">
<Link href={type['@id']}>
{type.title}
<ChevronrightIcon />
</Link>
</li>
))}
</ul>
</Popover>
);
};
168 changes: 168 additions & 0 deletions src/components/Contents/Contents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import React from 'react';
import './styles/basic/main.css';
import './styles/quanta/main.css';
import type { ActionsResponse } from '@plone/types';
import { ComponentProps, ReactNode, useState } from 'react';
import {
DialogTrigger,
Tooltip,
TooltipTrigger,
useDragAndDrop,
} from 'react-aria-components';
import cx from 'classnames';
import {
AddIcon,
Breadcrumbs,
Button,
Container,
QuantaTextField,
} from '@plone/components';
import { Table } from '../Table/Table';
import { ContentsCell } from './ContentsCell';
import { AddContentPopover } from './AddContentPopover';
import { indexes, defaultIndexes } from '../../helpers/indexes';
import type { ArrayElement, Brain } from '../../helpers/types';

interface ContentsProps {
pathname: string;
objectActions: ActionsResponse['object'];
loading: boolean;
title: string;
items: Brain[];
orderContent: (baseUrl: string, id: string, delta: number) => Promise<void>;
addableTypes: ComponentProps<typeof AddContentPopover>['addableTypes'];
}

/**
* A table showing the contents of an object.
*
* It has a toolbar for interactions with the items and a searchbar for filtering.
* Items can be sorted by drag and drop.
*/
export function Contents({
pathname,
objectActions,
loading,
title,
items,
orderContent,
addableTypes,
}: ContentsProps) {
const [selected, setSelected] = useState<string[]>([]);
// const path = getBaseUrl(pathname);
const path = pathname;

const folderContentsActions = objectActions.find(
(action) => action.id === 'folderContents',
);

if (!folderContentsActions) {
// TODO current volto returns the Unauthorized component here
// it would be best if the permissions check was done at a higher level
// and this remained null
return null;
}

const columns = [
{
id: 'title',
name: 'Title',
isRowHeader: true,
},
...defaultIndexes.map((index) => ({
id: index,
name: indexes[index].label,
})),
{
id: '_actions',
name: 'Actions',
},
] as const;

const rows = items.map((item) =>
columns.reduce<ArrayElement<ComponentProps<typeof Table>['rows']>>(
(cells, column) => ({
...cells,
[column.id]: (
<ContentsCell key={column.id} item={item} column={column.id} />
),
}),
{ id: item['@id'] },
),
);

const { dragAndDropHooks } = useDragAndDrop({
getItems: (keys) =>
[...keys].map((key) => ({
'text/plain': key.toString(),
})),
onReorder(e) {
if (e.keys.size !== 1) {
console.error('Only one item can be moved at a time');
}
const item = items.find((item) => item['@id'] === [...e.keys][0]);
if (!item) return;

const initialPosition = rows.findIndex((row) => row.id === item['@id']);
if (initialPosition === -1) return;

let finalPosition = rows.findIndex((row) => row.id === e.target.key);
if (e.target.dropPosition === 'after') finalPosition += 1;

orderContent(
path,
item.id.replace(/^.*\//, ''),
finalPosition - initialPosition,
);
},
});

return (
<Container
as="div"
// id="page-contents"
className="folder-contents"
aria-live="polite"
layout={false}
narrow={false}
>
{/* TODO better loader */}
{loading && <p>Loading...</p>}
{/* TODO helmet setting title here... or should we do it at a higher level? */}
<article id="content">
<section className="topbar">
<div className="title-block">
<Breadcrumbs includeRoot={true} items={[]} />
<h1>{title}</h1>
</div>
<QuantaTextField
name="sortable_title"
placeholder="Search site"
className="search-input"
/>
<TooltipTrigger>
<DialogTrigger>
<Button className="react-aria-Button add">
<AddIcon />
</Button>
<AddContentPopover addableTypes={addableTypes} />
</DialogTrigger>
<Tooltip className="react-aria-Tooltip tooltip" placement="bottom">
Add content
</Tooltip>
</TooltipTrigger>
</section>
<section className="contents-table">
<Table
columns={[...columns]}
rows={rows}
selectionMode="multiple"
dragAndDropHooks={dragAndDropHooks}
// onRowSelection={onRowSelection}
// resizableColumns={true}
/>
</section>
</article>
</Container>
);
}
77 changes: 77 additions & 0 deletions src/components/Contents/ContentsCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React from 'react';
import { DialogTrigger } from 'react-aria-components';
import { Brain } from '../../helpers/types';
import { Button, Link, MoreoptionsIcon, PageIcon } from '@plone/components';
import { indexes } from '../../helpers/indexes';
import { ItemActionsPopover } from './ItemActionsPopover';

interface Props {
item: Brain;
column: keyof typeof indexes | 'title' | '_actions';
}

export function ContentsCell({ item, column }: Props) {
if (column === 'title') {
return (
<Link
className="title-link"
href={`${item['@id']}${item.is_folderish ? '/contents' : ''}`}
>
<PageIcon />
{item.title}
{item.ExpirationDate !== 'None' &&
new Date(item.ExpirationDate).getTime() < new Date().getTime() && (
<span className="expired">Expired</span>
)}
{item.EffectiveDate !== 'None' &&
new Date(item.EffectiveDate).getTime() > new Date().getTime() && (
<span className="future">Scheduled</span>
)}
</Link>
);
} else if (column === '_actions') {
return (
<DialogTrigger>
<Button aria-label="More options">
<MoreoptionsIcon />
</Button>
<ItemActionsPopover
editLink={`${item['@id']}/edit`}
viewLink={item['@id']}
onMoveToBottom={async () => {}}
onMoveToTop={async () => {}}
onCopy={async () => {}}
onCut={async () => {}}
onDelete={async () => {}}
/>
</DialogTrigger>
);
} else {
if (indexes[column].type === 'boolean') {
return item[column] ? 'Yes' : 'No';
} else if (indexes[column].type === 'string') {
if (column !== 'review_state') {
return item[column];
} else {
return (
<div>
<span>
{/* <Dot color={getColor(item[index.id])} size="15px" /> */}
</span>
{item[column] || 'No workflow state'}
</div>
);
}
} else if (indexes[column].type === 'date') {
if (item[column] && item[column] !== 'None') {
// @ts-ignore TODO fix this, maybe a more strict type for the indexes?
return new Date(item[column]).toLocaleDateString();
} else {
return 'None';
}
} else if (indexes[column].type === 'array') {
const value = item[column];
return Array.isArray(value) ? value.join(', ') : value;
}
}
}
82 changes: 82 additions & 0 deletions src/components/Contents/ItemActionsPopover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React from 'react';
import {
Link,
Button,
Popover,
EditIcon,
EyeIcon,
RowbeforeIcon,
RowafterIcon,
CutIcon,
CopyIcon,
BinIcon,
} from '@plone/components';

interface Props {
editLink: string;
viewLink: string;
onMoveToTop: () => Promise<void>;
onMoveToBottom: () => Promise<void>;
onCut: () => Promise<void>;
onCopy: () => Promise<void>;
onDelete: () => Promise<void>;
}

export function ItemActionsPopover({
editLink,
viewLink,
onMoveToTop,
onMoveToBottom,
onCut,
onCopy,
onDelete,
}: Props) {
return (
<Popover className="react-aria-Popover item-actions-popover">
<ul className="item-actions-list">
<li className="item-actions-list-item edit">
<Link href={editLink}>
<EditIcon />
Edit
</Link>
</li>
<li className="item-actions-list-item view">
<Link href={viewLink}>
<EyeIcon />
View
</Link>
</li>
<li className="item-actions-list-item move-to-top">
<Button onPress={onMoveToTop}>
<RowbeforeIcon />
Move to top
</Button>
</li>
<li className="item-actions-list-item move-to-bottom">
<Button onPress={onMoveToBottom}>
<RowafterIcon />
Move to bottom
</Button>
</li>
<li className="item-actions-list-item cut">
<Button onPress={onCut}>
<CutIcon />
Cut
</Button>
</li>
<li className="item-actions-list-item copy">
<Button onPress={onCopy}>
<CopyIcon />
Copy
</Button>
</li>
<li className="item-actions-list-item delete">
<Button onPress={onDelete}>
<BinIcon />
Delete
</Button>
</li>
</ul>
</Popover>
);
}
Loading

0 comments on commit 905c0fe

Please sign in to comment.