@plone/volto 17.0.0-alpha.13 → 17.0.0-alpha.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.yarn/install-state.gz +0 -0
- package/CHANGELOG.md +51 -0
- package/README.md +2 -2
- package/docker-compose.yml +1 -1
- package/locales/ca/LC_MESSAGES/volto.po +5 -0
- package/locales/ca.json +1 -1
- package/locales/de/LC_MESSAGES/volto.po +5 -0
- package/locales/de.json +1 -1
- package/locales/en/LC_MESSAGES/volto.po +5 -0
- package/locales/en.json +1 -1
- package/locales/es/LC_MESSAGES/volto.po +49 -44
- package/locales/es.json +1 -1
- package/locales/eu/LC_MESSAGES/volto.po +5 -0
- package/locales/eu.json +1 -1
- package/locales/fi/LC_MESSAGES/volto.po +5 -0
- package/locales/fi.json +1 -1
- package/locales/fr/LC_MESSAGES/volto.po +5 -0
- package/locales/fr.json +1 -1
- package/locales/it/LC_MESSAGES/volto.po +5 -0
- package/locales/it.json +1 -1
- package/locales/ja/LC_MESSAGES/volto.po +5 -0
- package/locales/ja.json +1 -1
- package/locales/nl/LC_MESSAGES/volto.po +5 -0
- package/locales/nl.json +1 -1
- package/locales/pt/LC_MESSAGES/volto.po +5 -0
- package/locales/pt.json +1 -1
- package/locales/pt_BR/LC_MESSAGES/volto.po +5 -0
- package/locales/pt_BR.json +1 -1
- package/locales/ro/LC_MESSAGES/volto.po +5 -0
- package/locales/ro.json +1 -1
- package/locales/volto.pot +5 -0
- package/locales/zh_CN/LC_MESSAGES/volto.po +5 -0
- package/locales/zh_CN.json +1 -1
- package/package.json +2 -1
- package/packages/volto-slate/package.json +1 -1
- package/packages/volto-slate/src/blocks/Text/TextBlockView.jsx +20 -16
- package/packages/volto-slate/src/editor/config.jsx +5 -4
- package/packages/volto-slate/src/editor/less/slate.less +28 -0
- package/packages/volto-slate/src/editor/render.jsx +68 -8
- package/src/components/manage/Blocks/Listing/ListingBody.jsx +30 -8
- package/src/components/manage/Blocks/Title/View.jsx +15 -5
- package/src/components/manage/Blocks/Title/View.test.jsx +16 -1
- package/src/components/manage/Blocks/ToC/View.jsx +8 -1
- package/src/components/manage/Blocks/ToC/variations/DefaultTocRenderer.jsx +17 -4
- package/src/components/manage/Blocks/ToC/variations/HorizontalMenu.jsx +6 -2
- package/src/components/theme/Anontools/Anontools.jsx +45 -72
- package/src/components/theme/Anontools/Anontools.test.jsx +16 -2
- package/src/helpers/MessageLabels/MessageLabels.js +4 -0
- package/src/helpers/ScrollToTop/ScrollToTop.jsx +5 -3
- package/src/hooks/clipboard/useClipboard.js +26 -0
- package/src/hooks/content/useContent.js +31 -0
- package/src/hooks/index.js +2 -0
|
@@ -1,26 +1,30 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
serializeNodes,
|
|
3
|
+
serializeNodesToText,
|
|
4
|
+
} from '@plone/volto-slate/editor/render';
|
|
2
5
|
import config from '@plone/volto/registry';
|
|
6
|
+
import { isEqual } from 'lodash';
|
|
7
|
+
import Slugger from 'github-slugger';
|
|
3
8
|
|
|
4
9
|
const TextBlockView = (props) => {
|
|
5
10
|
const { id, data, styling = {} } = props;
|
|
6
11
|
const { value, override_toc } = data;
|
|
7
12
|
const metadata = props.metadata || props.properties;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
res.id = id;
|
|
18
|
-
}
|
|
13
|
+
const { topLevelTargetElements } = config.settings.slate;
|
|
14
|
+
|
|
15
|
+
const getAttributes = (node, path) => {
|
|
16
|
+
const res = { ...styling };
|
|
17
|
+
if (node.type && isEqual(path, [0])) {
|
|
18
|
+
if (topLevelTargetElements.includes(node.type) || override_toc) {
|
|
19
|
+
const text = serializeNodesToText(node?.children || []);
|
|
20
|
+
const slug = Slugger.slug(text);
|
|
21
|
+
res.id = slug || id;
|
|
19
22
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
}
|
|
24
|
+
return res;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
return serializeNodes(value, getAttributes, { metadata: metadata });
|
|
24
28
|
};
|
|
25
29
|
|
|
26
30
|
export default TextBlockView;
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
bTagDeserializer,
|
|
44
44
|
codeTagDeserializer,
|
|
45
45
|
} from './deserialize';
|
|
46
|
+
import { renderLinkElement } from './render';
|
|
46
47
|
|
|
47
48
|
// Registry of available buttons
|
|
48
49
|
export const buttons = {
|
|
@@ -234,10 +235,10 @@ export const defaultBlockType = 'p';
|
|
|
234
235
|
export const elements = {
|
|
235
236
|
default: ({ attributes, children }) => <p {...attributes}>{children}</p>,
|
|
236
237
|
|
|
237
|
-
h1: (
|
|
238
|
-
h2: (
|
|
239
|
-
h3: (
|
|
240
|
-
h4: (
|
|
238
|
+
h1: renderLinkElement('h1'),
|
|
239
|
+
h2: renderLinkElement('h2'),
|
|
240
|
+
h3: renderLinkElement('h3'),
|
|
241
|
+
h4: renderLinkElement('h4'),
|
|
241
242
|
|
|
242
243
|
li: ({ attributes, children }) => <li {...attributes}>{children}</li>,
|
|
243
244
|
ol: ({ attributes, children }) => <ol {...attributes}>{children}</ol>,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
h1,
|
|
2
|
+
h2,
|
|
3
|
+
h3,
|
|
4
|
+
h4 {
|
|
5
|
+
&:hover {
|
|
6
|
+
a.anchor {
|
|
7
|
+
svg {
|
|
8
|
+
opacity: 1;
|
|
9
|
+
transform: rotate(15deg);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
a.anchor {
|
|
15
|
+
position: absolute;
|
|
16
|
+
display: inline-block;
|
|
17
|
+
margin-left: 5px;
|
|
18
|
+
vertical-align: middle;
|
|
19
|
+
|
|
20
|
+
svg {
|
|
21
|
+
width: 1.6ch;
|
|
22
|
+
fill: #42526e;
|
|
23
|
+
opacity: 0;
|
|
24
|
+
transform: rotate(15deg) translate(-8px, 2px);
|
|
25
|
+
transition: opacity 0.2s ease 0s, transform 0.2s ease 0s;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
3
|
+
import { useLocation } from 'react-router-dom';
|
|
4
|
+
import { toast } from 'react-toastify';
|
|
5
|
+
import { useIntl } from 'react-intl';
|
|
3
6
|
import { Node, Text } from 'slate';
|
|
4
7
|
import cx from 'classnames';
|
|
5
|
-
import { isEmpty,
|
|
8
|
+
import { isEmpty, omit } from 'lodash';
|
|
9
|
+
import { UniversalLink, Toast } from '@plone/volto/components';
|
|
10
|
+
import { messages, addAppURL } from '@plone/volto/helpers';
|
|
11
|
+
import useClipboard from '@plone/volto/hooks/clipboard/useClipboard';
|
|
6
12
|
import config from '@plone/volto/registry';
|
|
13
|
+
import linkSVG from '@plone/volto/icons/link.svg';
|
|
14
|
+
|
|
15
|
+
import './less/slate.less';
|
|
7
16
|
|
|
8
17
|
const OMITTED = ['editor', 'path'];
|
|
9
18
|
|
|
@@ -106,13 +115,7 @@ export const serializeNodes = (nodes, getAttributes, extras = {}) => {
|
|
|
106
115
|
mode="view"
|
|
107
116
|
key={path}
|
|
108
117
|
data-slate-data={node.data ? serializeData(node) : null}
|
|
109
|
-
attributes={
|
|
110
|
-
isEqual(path, [0])
|
|
111
|
-
? getAttributes
|
|
112
|
-
? getAttributes(node, path)
|
|
113
|
-
: null
|
|
114
|
-
: null
|
|
115
|
-
}
|
|
118
|
+
attributes={getAttributes ? getAttributes(node, path) : null}
|
|
116
119
|
extras={extras}
|
|
117
120
|
>
|
|
118
121
|
{_serializeNodes(Array.from(Node.children(editor, path)))}
|
|
@@ -153,3 +156,60 @@ export const serializeNodesToText = (nodes) => {
|
|
|
153
156
|
|
|
154
157
|
export const serializeNodesToHtml = (nodes) =>
|
|
155
158
|
renderToStaticMarkup(serializeNodes(nodes));
|
|
159
|
+
|
|
160
|
+
export const renderLinkElement = (tagName) => {
|
|
161
|
+
function LinkElement({
|
|
162
|
+
attributes,
|
|
163
|
+
children,
|
|
164
|
+
mode = 'edit',
|
|
165
|
+
className = null,
|
|
166
|
+
}) {
|
|
167
|
+
const { slate = {} } = config.settings;
|
|
168
|
+
const Tag = tagName;
|
|
169
|
+
const slug = attributes.id || '';
|
|
170
|
+
const location = useLocation();
|
|
171
|
+
const appPathname = addAppURL(location.pathname);
|
|
172
|
+
// eslint-disable-next-line no-unused-vars
|
|
173
|
+
const [copied, copy, setCopied] = useClipboard(
|
|
174
|
+
appPathname.concat(`#${slug}`),
|
|
175
|
+
);
|
|
176
|
+
const intl = useIntl();
|
|
177
|
+
|
|
178
|
+
return slate.useLinkedHeadings === false ? (
|
|
179
|
+
<Tag {...attributes} className={className}>
|
|
180
|
+
{children}
|
|
181
|
+
</Tag>
|
|
182
|
+
) : (
|
|
183
|
+
<Tag {...attributes} className={className}>
|
|
184
|
+
{children}
|
|
185
|
+
{mode === 'view' && slug && (
|
|
186
|
+
<UniversalLink
|
|
187
|
+
className="anchor"
|
|
188
|
+
aria-hidden="true"
|
|
189
|
+
tabIndex={-1}
|
|
190
|
+
href={`#${slug}`}
|
|
191
|
+
>
|
|
192
|
+
<svg
|
|
193
|
+
{...linkSVG.attributes}
|
|
194
|
+
dangerouslySetInnerHTML={{ __html: linkSVG.content }}
|
|
195
|
+
height={null}
|
|
196
|
+
onClick={() => {
|
|
197
|
+
copy();
|
|
198
|
+
|
|
199
|
+
toast.info(
|
|
200
|
+
<Toast
|
|
201
|
+
info
|
|
202
|
+
title={intl.formatMessage(messages.success)}
|
|
203
|
+
content={intl.formatMessage(messages.urlClipboardCopy)}
|
|
204
|
+
/>,
|
|
205
|
+
);
|
|
206
|
+
}}
|
|
207
|
+
></svg>
|
|
208
|
+
</UniversalLink>
|
|
209
|
+
)}
|
|
210
|
+
</Tag>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
LinkElement.displayName = `${tagName}LinkElement`;
|
|
214
|
+
return LinkElement;
|
|
215
|
+
};
|
|
@@ -1,14 +1,35 @@
|
|
|
1
|
-
import React, { createRef } from 'react';
|
|
1
|
+
import React, { createRef, useMemo } from 'react';
|
|
2
2
|
import { FormattedMessage, injectIntl } from 'react-intl';
|
|
3
3
|
import cx from 'classnames';
|
|
4
4
|
import { Pagination, Dimmer, Loader } from 'semantic-ui-react';
|
|
5
|
+
import Slugger from 'github-slugger';
|
|
5
6
|
import { Icon } from '@plone/volto/components';
|
|
7
|
+
import { renderLinkElement } from '@plone/volto-slate/editor/render';
|
|
6
8
|
import config from '@plone/volto/registry';
|
|
7
9
|
import withQuerystringResults from './withQuerystringResults';
|
|
8
10
|
|
|
9
11
|
import paginationLeftSVG from '@plone/volto/icons/left-key.svg';
|
|
10
12
|
import paginationRightSVG from '@plone/volto/icons/right-key.svg';
|
|
11
13
|
|
|
14
|
+
const Headline = ({ headlineTag, id, data = {}, listingItems, isEditMode }) => {
|
|
15
|
+
let attr = { id };
|
|
16
|
+
const slug = Slugger.slug(data.headline);
|
|
17
|
+
attr.id = slug || id;
|
|
18
|
+
const LinkedHeadline = useMemo(() => renderLinkElement(headlineTag), [
|
|
19
|
+
headlineTag,
|
|
20
|
+
]);
|
|
21
|
+
return (
|
|
22
|
+
<LinkedHeadline
|
|
23
|
+
mode={!isEditMode && 'view'}
|
|
24
|
+
children={data.headline}
|
|
25
|
+
attributes={attr}
|
|
26
|
+
className={cx('headline', {
|
|
27
|
+
emptyListing: !listingItems?.length > 0,
|
|
28
|
+
})}
|
|
29
|
+
/>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
12
33
|
const ListingBody = withQuerystringResults((props) => {
|
|
13
34
|
const {
|
|
14
35
|
data = {},
|
|
@@ -22,6 +43,7 @@ const ListingBody = withQuerystringResults((props) => {
|
|
|
22
43
|
nextBatch,
|
|
23
44
|
isFolderContentsListing,
|
|
24
45
|
hasLoaded,
|
|
46
|
+
id,
|
|
25
47
|
} = props;
|
|
26
48
|
|
|
27
49
|
let ListingBodyTemplate;
|
|
@@ -50,13 +72,13 @@ const ListingBody = withQuerystringResults((props) => {
|
|
|
50
72
|
return (
|
|
51
73
|
<>
|
|
52
74
|
{data.headline && (
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
{
|
|
59
|
-
|
|
75
|
+
<Headline
|
|
76
|
+
headlineTag={HeadlineTag}
|
|
77
|
+
id={id}
|
|
78
|
+
listingItems={listingItems}
|
|
79
|
+
data={data}
|
|
80
|
+
isEditMode={isEditMode}
|
|
81
|
+
/>
|
|
60
82
|
)}
|
|
61
83
|
{listingItems?.length > 0 ? (
|
|
62
84
|
<div ref={listingRef}>
|
|
@@ -3,19 +3,29 @@
|
|
|
3
3
|
* @module volto-slate/blocks/Title/TitleBlockView
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import React from 'react';
|
|
6
|
+
import React, { useMemo } from 'react';
|
|
7
7
|
import PropTypes from 'prop-types';
|
|
8
|
+
import Slugger from 'github-slugger';
|
|
9
|
+
import { renderLinkElement } from '@plone/volto-slate/editor/render';
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* View title block component.
|
|
11
13
|
* @class View
|
|
12
14
|
* @extends Component
|
|
13
15
|
*/
|
|
14
|
-
const TitleBlockView = ({ properties, metadata }) => {
|
|
16
|
+
const TitleBlockView = ({ properties, metadata, id, children }) => {
|
|
17
|
+
let attr = { id };
|
|
18
|
+
const title = (properties || metadata)['title'];
|
|
19
|
+
const slug = Slugger.slug(title);
|
|
20
|
+
attr.id = slug || id;
|
|
21
|
+
const LinkedTitle = useMemo(() => renderLinkElement('h1'), []);
|
|
15
22
|
return (
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
23
|
+
<LinkedTitle
|
|
24
|
+
mode="view"
|
|
25
|
+
children={title ?? children}
|
|
26
|
+
attributes={attr}
|
|
27
|
+
className={'documentFirstHeading'}
|
|
28
|
+
/>
|
|
19
29
|
);
|
|
20
30
|
};
|
|
21
31
|
|
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import renderer from 'react-test-renderer';
|
|
3
|
+
import configureStore from 'redux-mock-store';
|
|
4
|
+
import { Provider } from 'react-intl-redux';
|
|
5
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
3
6
|
import View from './View';
|
|
4
7
|
|
|
8
|
+
const mockStore = configureStore();
|
|
9
|
+
|
|
5
10
|
test('renders a view title component', () => {
|
|
11
|
+
const store = mockStore({
|
|
12
|
+
intl: {
|
|
13
|
+
locale: 'en',
|
|
14
|
+
messages: {},
|
|
15
|
+
},
|
|
16
|
+
});
|
|
6
17
|
const component = renderer.create(
|
|
7
|
-
<
|
|
18
|
+
<Provider store={store}>
|
|
19
|
+
<MemoryRouter>
|
|
20
|
+
<View properties={{ title: 'My Title' }} id="a123" />
|
|
21
|
+
</MemoryRouter>
|
|
22
|
+
</Provider>,
|
|
8
23
|
);
|
|
9
24
|
const json = component.toJSON();
|
|
10
25
|
expect(json).toMatchSnapshot();
|
|
@@ -56,7 +56,14 @@ const View = (props) => {
|
|
|
56
56
|
const items = [];
|
|
57
57
|
if (!level || !levels.includes(level)) return;
|
|
58
58
|
tocEntriesLayout.push(id);
|
|
59
|
-
tocEntries[id] = {
|
|
59
|
+
tocEntries[id] = {
|
|
60
|
+
level,
|
|
61
|
+
title: title || block.plaintext,
|
|
62
|
+
items,
|
|
63
|
+
id,
|
|
64
|
+
override_toc: block.override_toc,
|
|
65
|
+
plaintext: block.plaintext,
|
|
66
|
+
};
|
|
60
67
|
if (level < rootLevel) {
|
|
61
68
|
rootLevel = level;
|
|
62
69
|
}
|
|
@@ -8,15 +8,27 @@ import PropTypes from 'prop-types';
|
|
|
8
8
|
import { map } from 'lodash';
|
|
9
9
|
import { List } from 'semantic-ui-react';
|
|
10
10
|
import { FormattedMessage, injectIntl } from 'react-intl';
|
|
11
|
+
import { useHistory } from 'react-router-dom';
|
|
11
12
|
import AnchorLink from 'react-anchor-link-smooth-scroll';
|
|
13
|
+
import Slugger from 'github-slugger';
|
|
12
14
|
|
|
13
|
-
const RenderListItems = ({ items, data }) => {
|
|
15
|
+
const RenderListItems = ({ items, data, history }) => {
|
|
14
16
|
return map(items, (item) => {
|
|
15
|
-
const { id, level, title } = item;
|
|
17
|
+
const { id, level, title, override_toc, plaintext } = item;
|
|
18
|
+
const slug = override_toc
|
|
19
|
+
? Slugger.slug(plaintext)
|
|
20
|
+
: Slugger.slug(title) || id;
|
|
16
21
|
return (
|
|
17
22
|
item && (
|
|
18
23
|
<List.Item key={id} className={`item headline-${level}`} as="li">
|
|
19
|
-
<AnchorLink
|
|
24
|
+
<AnchorLink
|
|
25
|
+
href={`#${slug}`}
|
|
26
|
+
onClick={(e) => {
|
|
27
|
+
history.push({ hash: slug });
|
|
28
|
+
}}
|
|
29
|
+
>
|
|
30
|
+
{title}
|
|
31
|
+
</AnchorLink>
|
|
20
32
|
{item.items?.length > 0 && (
|
|
21
33
|
<List
|
|
22
34
|
ordered={data.ordered}
|
|
@@ -38,6 +50,7 @@ const RenderListItems = ({ items, data }) => {
|
|
|
38
50
|
* @extends Component
|
|
39
51
|
*/
|
|
40
52
|
const View = ({ data, tocEntries }) => {
|
|
53
|
+
const history = useHistory();
|
|
41
54
|
return (
|
|
42
55
|
<>
|
|
43
56
|
{data.title && !data.hide_title ? (
|
|
@@ -57,7 +70,7 @@ const View = ({ data, tocEntries }) => {
|
|
|
57
70
|
bulleted={!data.ordered}
|
|
58
71
|
as={data.ordered ? 'ol' : 'ul'}
|
|
59
72
|
>
|
|
60
|
-
<RenderListItems items={tocEntries} data={data} />
|
|
73
|
+
<RenderListItems items={tocEntries} data={data} history={history} />
|
|
61
74
|
</List>
|
|
62
75
|
</>
|
|
63
76
|
);
|
|
@@ -9,15 +9,19 @@ import { map } from 'lodash';
|
|
|
9
9
|
import { Menu } from 'semantic-ui-react';
|
|
10
10
|
import { FormattedMessage, injectIntl } from 'react-intl';
|
|
11
11
|
import AnchorLink from 'react-anchor-link-smooth-scroll';
|
|
12
|
+
import Slugger from 'github-slugger';
|
|
12
13
|
|
|
13
14
|
const RenderMenuItems = ({ items }) => {
|
|
14
15
|
return map(items, (item) => {
|
|
15
|
-
const { id, level, title } = item;
|
|
16
|
+
const { id, level, title, override_toc, plaintext } = item;
|
|
17
|
+
const slug = override_toc
|
|
18
|
+
? Slugger.slug(plaintext)
|
|
19
|
+
: Slugger.slug(title) || id;
|
|
16
20
|
return (
|
|
17
21
|
item && (
|
|
18
22
|
<React.Fragment key={id}>
|
|
19
23
|
<Menu.Item className={`headline-${level}`}>
|
|
20
|
-
<AnchorLink href={`#${
|
|
24
|
+
<AnchorLink href={`#${slug}`}>{title}</AnchorLink>
|
|
21
25
|
</Menu.Item>
|
|
22
26
|
{item.items?.length > 0 && <RenderMenuItems items={item.items} />}
|
|
23
27
|
</React.Fragment>
|
|
@@ -1,83 +1,56 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Anontools component.
|
|
3
|
-
* @module components/theme/Anontools/Anontools
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import React, { Component } from 'react';
|
|
7
1
|
import PropTypes from 'prop-types';
|
|
8
|
-
import { connect } from 'react-redux';
|
|
9
2
|
import { Link } from 'react-router-dom';
|
|
10
3
|
import { Menu } from 'semantic-ui-react';
|
|
11
4
|
import { FormattedMessage } from 'react-intl';
|
|
5
|
+
import { flattenToAppURL } from '@plone/volto/helpers';
|
|
6
|
+
import { useToken } from '@plone/volto/hooks/userSession/useToken';
|
|
7
|
+
import { useContent } from '@plone/volto/hooks/content/useContent';
|
|
12
8
|
import config from '@plone/volto/registry';
|
|
13
9
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
export class Anontools extends Component {
|
|
18
|
-
/**
|
|
19
|
-
* Property types.
|
|
20
|
-
* @property {Object} propTypes Property types.
|
|
21
|
-
* @static
|
|
22
|
-
*/
|
|
23
|
-
static propTypes = {
|
|
24
|
-
token: PropTypes.string,
|
|
25
|
-
content: PropTypes.shape({
|
|
26
|
-
'@id': PropTypes.string,
|
|
27
|
-
}),
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Default properties.
|
|
32
|
-
* @property {Object} defaultProps Default properties.
|
|
33
|
-
* @static
|
|
34
|
-
*/
|
|
35
|
-
static defaultProps = {
|
|
36
|
-
token: null,
|
|
37
|
-
content: {
|
|
38
|
-
'@id': null,
|
|
39
|
-
},
|
|
40
|
-
};
|
|
10
|
+
const Anontools = () => {
|
|
11
|
+
const token = useToken();
|
|
12
|
+
const { data: content } = useContent();
|
|
41
13
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
14
|
+
const { settings } = config;
|
|
15
|
+
return (
|
|
16
|
+
!token && (
|
|
17
|
+
<Menu pointing secondary floated="right">
|
|
18
|
+
<Menu.Item>
|
|
19
|
+
<Link
|
|
20
|
+
aria-label="login"
|
|
21
|
+
to={`/login${
|
|
22
|
+
content?.['@id']
|
|
23
|
+
? `?return_url=${flattenToAppURL(content['@id'])}`
|
|
24
|
+
: ''
|
|
25
|
+
}`}
|
|
26
|
+
>
|
|
27
|
+
<FormattedMessage id="Log in" defaultMessage="Log in" />
|
|
28
|
+
</Link>
|
|
29
|
+
</Menu.Item>
|
|
30
|
+
{settings.showSelfRegistration && (
|
|
52
31
|
<Menu.Item>
|
|
53
|
-
<Link
|
|
54
|
-
|
|
55
|
-
to={`/login${
|
|
56
|
-
this.props.content?.['@id']
|
|
57
|
-
? `?return_url=${this.props.content['@id'].replace(
|
|
58
|
-
settings.apiPath,
|
|
59
|
-
'',
|
|
60
|
-
)}`
|
|
61
|
-
: ''
|
|
62
|
-
}`}
|
|
63
|
-
>
|
|
64
|
-
<FormattedMessage id="Log in" defaultMessage="Log in" />
|
|
32
|
+
<Link aria-label="register" to="/register">
|
|
33
|
+
<FormattedMessage id="Register" defaultMessage="Register" />
|
|
65
34
|
</Link>
|
|
66
35
|
</Menu.Item>
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
36
|
+
)}
|
|
37
|
+
</Menu>
|
|
38
|
+
)
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default Anontools;
|
|
43
|
+
|
|
44
|
+
Anontools.propTypes = {
|
|
45
|
+
token: PropTypes.string,
|
|
46
|
+
content: PropTypes.shape({
|
|
47
|
+
'@id': PropTypes.string,
|
|
48
|
+
}),
|
|
49
|
+
};
|
|
79
50
|
|
|
80
|
-
|
|
81
|
-
token:
|
|
82
|
-
content:
|
|
83
|
-
|
|
51
|
+
Anontools.defaultProps = {
|
|
52
|
+
token: null,
|
|
53
|
+
content: {
|
|
54
|
+
'@id': null,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
@@ -12,7 +12,14 @@ describe('Anontools', () => {
|
|
|
12
12
|
it('renders an anontools component when no token is specified', () => {
|
|
13
13
|
const store = mockStore({
|
|
14
14
|
userSession: { token: null },
|
|
15
|
-
content: {
|
|
15
|
+
content: {
|
|
16
|
+
data: { '@id': 'myid' },
|
|
17
|
+
get: {
|
|
18
|
+
loading: false,
|
|
19
|
+
loaded: true,
|
|
20
|
+
error: null,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
16
23
|
intl: {
|
|
17
24
|
locale: 'en',
|
|
18
25
|
messages: {},
|
|
@@ -32,7 +39,14 @@ describe('Anontools', () => {
|
|
|
32
39
|
it('should not render an anontools component when a token is specified', () => {
|
|
33
40
|
const store = mockStore({
|
|
34
41
|
userSession: { token: '1234' },
|
|
35
|
-
content: {
|
|
42
|
+
content: {
|
|
43
|
+
data: {},
|
|
44
|
+
get: {
|
|
45
|
+
loading: false,
|
|
46
|
+
loaded: true,
|
|
47
|
+
error: null,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
36
50
|
intl: {
|
|
37
51
|
locale: 'en',
|
|
38
52
|
messages: {},
|
|
@@ -260,6 +260,10 @@ export const messages = defineMessages({
|
|
|
260
260
|
id: 'Show groups of users below',
|
|
261
261
|
defaultMessage: 'Show groups of users below',
|
|
262
262
|
},
|
|
263
|
+
urlClipboardCopy: {
|
|
264
|
+
id: 'Link copied to clipboard',
|
|
265
|
+
defaultMessage: 'Link copied to clipboard',
|
|
266
|
+
},
|
|
263
267
|
inspectRelations: {
|
|
264
268
|
id: 'Inspect relations',
|
|
265
269
|
defaultMessage: 'Inspect relations',
|
|
@@ -28,15 +28,17 @@ class ScrollToTop extends React.Component {
|
|
|
28
28
|
* @memberof ScrollToTop
|
|
29
29
|
*/
|
|
30
30
|
componentDidUpdate(prevProps) {
|
|
31
|
+
const { location } = this.props;
|
|
31
32
|
const noInitialBlocksFocus = // Do not scroll on /edit
|
|
32
33
|
config.blocks?.initialBlocksFocus === null
|
|
33
34
|
? this.props.location?.pathname.slice(-5) !== '/edit'
|
|
34
35
|
: true;
|
|
36
|
+
|
|
37
|
+
const isHash = location?.hash || location?.pathname.hash;
|
|
35
38
|
if (
|
|
36
|
-
!
|
|
37
|
-
!this.props.location?.pathname.hash &&
|
|
39
|
+
!isHash &&
|
|
38
40
|
noInitialBlocksFocus &&
|
|
39
|
-
|
|
41
|
+
location?.pathname !== prevProps.location?.pathname
|
|
40
42
|
) {
|
|
41
43
|
window.scrollTo(0, 0);
|
|
42
44
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
export default function useClipboard(clipboardText = '') {
|
|
4
|
+
const stringToCopy = useRef(clipboardText);
|
|
5
|
+
const [copied, setCopied] = useState(false);
|
|
6
|
+
|
|
7
|
+
//synchronous: window.clipboardData.setData(options.format || "text", text);
|
|
8
|
+
const copyToClipboard = async (text) => {
|
|
9
|
+
if ('clipboard' in navigator) {
|
|
10
|
+
return await navigator.clipboard.writeText(text);
|
|
11
|
+
} else {
|
|
12
|
+
return document.execCommand('copy', true, text);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const copyAction = useCallback(() => {
|
|
17
|
+
const copiedString = copyToClipboard(stringToCopy.current);
|
|
18
|
+
setCopied(copiedString);
|
|
19
|
+
}, [stringToCopy]);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
stringToCopy.current = clipboardText;
|
|
23
|
+
}, [clipboardText]);
|
|
24
|
+
|
|
25
|
+
return [copied, copyAction, setCopied];
|
|
26
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useSelector, shallowEqual } from 'react-redux';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useContent hook
|
|
5
|
+
*
|
|
6
|
+
* This hook returns the current content that is stored in the Redux store in the
|
|
7
|
+
* `content` reducer, and returns it along with the related state (loading/loaded/error).
|
|
8
|
+
*
|
|
9
|
+
* @export
|
|
10
|
+
* @return {{ data: ContentData, loading: boolean, loaded: boolean, error: Error }}
|
|
11
|
+
*/
|
|
12
|
+
export function useContent() {
|
|
13
|
+
const data = useSelector((state) => state.content.data, shallowEqual);
|
|
14
|
+
const loading = useSelector((state) => state.content.get.loading);
|
|
15
|
+
const loaded = useSelector((state) => state.content.get.loaded);
|
|
16
|
+
const error = useSelector((state) => state.content.get.error, shallowEqual);
|
|
17
|
+
|
|
18
|
+
return { data, loading, loaded, error };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// For reference purposes: Potential future useQuery version
|
|
22
|
+
// export function useContent() {
|
|
23
|
+
// // the cache will need to know the current location
|
|
24
|
+
// const pathname = useLocation();
|
|
25
|
+
// const query = useQuery(getContentQuery({ path }))
|
|
26
|
+
|
|
27
|
+
// // This might not be needed if we rename the properties
|
|
28
|
+
// const {isLoading: loading, isSuccess: loaded, ...rest} = query;
|
|
29
|
+
|
|
30
|
+
// return { loading, loaded, ...rest };
|
|
31
|
+
// }
|