react-cheminfo 0.59.0 → 0.60.0

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.
Files changed (125) hide show
  1. package/README.md +7 -2
  2. package/lib/about/ui/AboutBuildDetails.d.ts +15 -0
  3. package/lib/about/ui/AboutBuildDetails.d.ts.map +1 -0
  4. package/lib/about/ui/AboutBuildDetails.js +17 -0
  5. package/lib/about/ui/AboutBuildDetails.js.map +1 -0
  6. package/lib/about/ui/AboutPage.d.ts +8 -0
  7. package/lib/about/ui/AboutPage.d.ts.map +1 -1
  8. package/lib/about/ui/AboutPage.js +12 -3
  9. package/lib/about/ui/AboutPage.js.map +1 -1
  10. package/lib/about/ui/AboutSignIn.d.ts +33 -0
  11. package/lib/about/ui/AboutSignIn.d.ts.map +1 -0
  12. package/lib/about/ui/AboutSignIn.js +28 -0
  13. package/lib/about/ui/AboutSignIn.js.map +1 -0
  14. package/lib/about/ui/AboutVersion.d.ts +4 -2
  15. package/lib/about/ui/AboutVersion.d.ts.map +1 -1
  16. package/lib/about/ui/AboutVersion.js +21 -3
  17. package/lib/about/ui/AboutVersion.js.map +1 -1
  18. package/lib/about/ui/index.d.ts +2 -2
  19. package/lib/about/ui/index.d.ts.map +1 -1
  20. package/lib/about/ui/index.js +1 -1
  21. package/lib/about/ui/index.js.map +1 -1
  22. package/lib/chrome/ui/NavEntry.d.ts +16 -0
  23. package/lib/chrome/ui/NavEntry.d.ts.map +1 -0
  24. package/lib/chrome/ui/NavEntry.js +27 -0
  25. package/lib/chrome/ui/NavEntry.js.map +1 -0
  26. package/lib/chrome/ui/SiteHeader.d.ts +11 -4
  27. package/lib/chrome/ui/SiteHeader.d.ts.map +1 -1
  28. package/lib/chrome/ui/SiteHeader.js +4 -26
  29. package/lib/chrome/ui/SiteHeader.js.map +1 -1
  30. package/lib/core.d.ts +1 -0
  31. package/lib/core.d.ts.map +1 -1
  32. package/lib/core.js +1 -0
  33. package/lib/core.js.map +1 -1
  34. package/lib/i18n/core/chromeCatalog.d.ts +1 -1
  35. package/lib/i18n/core/chromeCatalog.d.ts.map +1 -1
  36. package/lib/locales/de.json +11 -3
  37. package/lib/locales/en.json +11 -3
  38. package/lib/locales/es.json +11 -3
  39. package/lib/locales/fr.json +11 -3
  40. package/lib/locales/it.json +11 -3
  41. package/lib/moleculesSources/core/index.d.ts +3 -0
  42. package/lib/moleculesSources/core/index.d.ts.map +1 -0
  43. package/lib/moleculesSources/core/index.js +2 -0
  44. package/lib/moleculesSources/core/index.js.map +1 -0
  45. package/lib/moleculesSources/core/moleculesSources.d.ts +55 -0
  46. package/lib/moleculesSources/core/moleculesSources.d.ts.map +1 -0
  47. package/lib/moleculesSources/core/moleculesSources.js +63 -0
  48. package/lib/moleculesSources/core/moleculesSources.js.map +1 -0
  49. package/lib/moleculesSources/ui/MoleculesSourceBadge.d.ts +18 -0
  50. package/lib/moleculesSources/ui/MoleculesSourceBadge.d.ts.map +1 -0
  51. package/lib/moleculesSources/ui/MoleculesSourceBadge.js +22 -0
  52. package/lib/moleculesSources/ui/MoleculesSourceBadge.js.map +1 -0
  53. package/lib/moleculesSources/ui/SourceBadge.d.ts +57 -0
  54. package/lib/moleculesSources/ui/SourceBadge.d.ts.map +1 -0
  55. package/lib/moleculesSources/ui/SourceBadge.js +29 -0
  56. package/lib/moleculesSources/ui/SourceBadge.js.map +1 -0
  57. package/lib/moleculesSources/ui/index.d.ts +7 -0
  58. package/lib/moleculesSources/ui/index.d.ts.map +1 -0
  59. package/lib/moleculesSources/ui/index.js +4 -0
  60. package/lib/moleculesSources/ui/index.js.map +1 -0
  61. package/lib/moleculesSources/ui/useSourceLooks.d.ts +24 -0
  62. package/lib/moleculesSources/ui/useSourceLooks.d.ts.map +1 -0
  63. package/lib/moleculesSources/ui/useSourceLooks.js +53 -0
  64. package/lib/moleculesSources/ui/useSourceLooks.js.map +1 -0
  65. package/lib/moleculesToken/core/index.d.ts +1 -0
  66. package/lib/moleculesToken/core/index.d.ts.map +1 -1
  67. package/lib/moleculesToken/core/index.js +1 -0
  68. package/lib/moleculesToken/core/index.js.map +1 -1
  69. package/lib/moleculesToken/core/tokenFix.d.ts +18 -0
  70. package/lib/moleculesToken/core/tokenFix.d.ts.map +1 -0
  71. package/lib/moleculesToken/core/tokenFix.js +27 -0
  72. package/lib/moleculesToken/core/tokenFix.js.map +1 -0
  73. package/lib/moleculesToken/ui/MoleculesTokenFix.d.ts +22 -0
  74. package/lib/moleculesToken/ui/MoleculesTokenFix.d.ts.map +1 -0
  75. package/lib/moleculesToken/ui/MoleculesTokenFix.js +35 -0
  76. package/lib/moleculesToken/ui/MoleculesTokenFix.js.map +1 -0
  77. package/lib/moleculesToken/ui/MoleculesTokenTable.d.ts +6 -1
  78. package/lib/moleculesToken/ui/MoleculesTokenTable.d.ts.map +1 -1
  79. package/lib/moleculesToken/ui/MoleculesTokenTable.js +7 -75
  80. package/lib/moleculesToken/ui/MoleculesTokenTable.js.map +1 -1
  81. package/lib/moleculesToken/ui/index.d.ts +2 -0
  82. package/lib/moleculesToken/ui/index.d.ts.map +1 -1
  83. package/lib/moleculesToken/ui/index.js +1 -0
  84. package/lib/moleculesToken/ui/index.js.map +1 -1
  85. package/lib/moleculesToken/ui/tokenDetails.d.ts +13 -0
  86. package/lib/moleculesToken/ui/tokenDetails.d.ts.map +1 -0
  87. package/lib/moleculesToken/ui/tokenDetails.js +73 -0
  88. package/lib/moleculesToken/ui/tokenDetails.js.map +1 -0
  89. package/lib/ui.d.ts +1 -0
  90. package/lib/ui.d.ts.map +1 -1
  91. package/lib/ui.js +1 -0
  92. package/lib/ui.js.map +1 -1
  93. package/package.json +1 -1
  94. package/src/about/ui/AboutBuildDetails.tsx +45 -0
  95. package/src/about/ui/AboutPage.tsx +27 -13
  96. package/src/about/ui/AboutSignIn.tsx +63 -0
  97. package/src/about/ui/AboutVersion.tsx +27 -9
  98. package/src/about/ui/index.ts +2 -2
  99. package/src/chrome/ui/NavEntry.tsx +41 -0
  100. package/src/chrome/ui/SiteHeader.tsx +15 -41
  101. package/src/core.ts +1 -0
  102. package/src/locales/de.json +11 -3
  103. package/src/locales/en.json +11 -3
  104. package/src/locales/es.json +11 -3
  105. package/src/locales/fr.json +11 -3
  106. package/src/locales/it.json +11 -3
  107. package/src/moleculesSources/core/index.ts +11 -0
  108. package/src/moleculesSources/core/moleculesSources.ts +102 -0
  109. package/src/moleculesSources/ui/MoleculesSourceBadge.tsx +44 -0
  110. package/src/moleculesSources/ui/SourceBadge.tsx +117 -0
  111. package/src/moleculesSources/ui/index.ts +6 -0
  112. package/src/moleculesSources/ui/useSourceLooks.ts +70 -0
  113. package/src/moleculesToken/core/index.ts +1 -0
  114. package/src/moleculesToken/core/tokenFix.ts +37 -0
  115. package/src/moleculesToken/ui/MoleculesTokenFix.tsx +95 -0
  116. package/src/moleculesToken/ui/MoleculesTokenTable.tsx +20 -94
  117. package/src/moleculesToken/ui/index.ts +2 -0
  118. package/src/moleculesToken/ui/tokenDetails.tsx +91 -0
  119. package/src/ui.ts +1 -0
  120. package/styles/chrome.css +159 -0
  121. package/lib/about/ui/AboutBuild.d.ts +0 -18
  122. package/lib/about/ui/AboutBuild.d.ts.map +0 -1
  123. package/lib/about/ui/AboutBuild.js +0 -37
  124. package/lib/about/ui/AboutBuild.js.map +0 -1
  125. package/src/about/ui/AboutBuild.tsx +0 -54
@@ -0,0 +1,44 @@
1
+ import type { ReactElement } from 'react';
2
+
3
+ import { MOLECULES_URL, sourceLook } from '../core/moleculesSources.ts';
4
+
5
+ import type { SourceBadgeProps } from './SourceBadge.tsx';
6
+ import { SourceBadge } from './SourceBadge.tsx';
7
+ import { useSourceLooks } from './useSourceLooks.ts';
8
+
9
+ export interface MoleculesSourceBadgeProps extends Omit<
10
+ SourceBadgeProps,
11
+ 'color' | 'logo'
12
+ > {
13
+ /**
14
+ * Where molecules answers, for a site that reads another instance.
15
+ * @default MOLECULES_URL
16
+ */
17
+ moleculesUrl?: string;
18
+ }
19
+
20
+ /**
21
+ * A source named by a site that does not keep its look: the colour and the
22
+ * logo come from molecules.cheminfo.org, where they are edited. A source
23
+ * molecules does not know — or until it has answered — is drawn neutral.
24
+ * @param props - The source, by its name in any case, and the entry it holds.
25
+ * @returns The badge.
26
+ */
27
+ export function MoleculesSourceBadge(
28
+ props: MoleculesSourceBadgeProps,
29
+ ): ReactElement {
30
+ const { moleculesUrl = MOLECULES_URL, ...badge } = props;
31
+ const look = sourceLook(useSourceLooks(moleculesUrl), badge.name);
32
+ if (look === undefined) return <SourceBadge {...badge} />;
33
+
34
+ return (
35
+ <SourceBadge
36
+ {...badge}
37
+ label={badge.label ?? look.name}
38
+ // A source molecules draws in no colour of its own is drawn neutral
39
+ // here: the site naming it is not that source.
40
+ color={look.color ?? undefined}
41
+ logo={look.logo}
42
+ />
43
+ );
44
+ }
@@ -0,0 +1,117 @@
1
+ import type { CSSProperties, ReactElement, ReactNode } from 'react';
2
+
3
+ export interface SourceBadgeProps {
4
+ /** What the source is called, read in the badge's tooltip. */
5
+ name: string;
6
+ /**
7
+ * What the badge says.
8
+ * @default name
9
+ */
10
+ label?: ReactNode;
11
+ /**
12
+ * The source's own colour, as `#rrggbb`. `null` draws it in the site's own
13
+ * colour, which is how a site marks a source that is itself.
14
+ * @default undefined — a neutral grey, for a source with no look
15
+ */
16
+ color?: string | null;
17
+ /**
18
+ * The absolute address of its little logo.
19
+ * @default null
20
+ */
21
+ logo?: string | null;
22
+ /**
23
+ * The entry's identifier there, written after the name.
24
+ * @default null
25
+ */
26
+ identifier?: string | null;
27
+ /**
28
+ * How many entries the source holds, written after the name when more than
29
+ * one.
30
+ * @default 1
31
+ */
32
+ count?: number;
33
+ /**
34
+ * Where the badge leads, opened in a new tab.
35
+ * @default null — the badge names the source and leads nowhere
36
+ */
37
+ href?: string | null;
38
+ /**
39
+ * The tooltip.
40
+ * @default the name, then the identifier
41
+ */
42
+ title?: string;
43
+ /**
44
+ * The smaller badge of a dense list, whose identifier goes in the tooltip
45
+ * only.
46
+ * @default false
47
+ */
48
+ compact?: boolean;
49
+ }
50
+
51
+ /**
52
+ * A source, as a badge: its name in its own colour, its little logo on a white
53
+ * chip, and the entry it holds — the same badge on every site of the family
54
+ * that names where a molecule comes from.
55
+ * @param props - See {@link SourceBadgeProps}.
56
+ * @returns The badge.
57
+ */
58
+ export function SourceBadge(props: SourceBadgeProps): ReactElement {
59
+ const {
60
+ name,
61
+ label = name,
62
+ color,
63
+ logo = null,
64
+ identifier = null,
65
+ count = 1,
66
+ href = null,
67
+ compact = false,
68
+ } = props;
69
+ const title =
70
+ props.title ?? (identifier === null ? name : `${name} ${identifier}`);
71
+ const className = [
72
+ 'source-badge',
73
+ compact ? 'source-badge--compact' : '',
74
+ color === undefined ? 'source-badge--neutral' : '',
75
+ ]
76
+ .filter(Boolean)
77
+ .join(' ');
78
+ // A source with no colour of its own takes the site's, from the stylesheet.
79
+ const style =
80
+ typeof color === 'string'
81
+ ? ({ '--source': color } as CSSProperties)
82
+ : undefined;
83
+ const content = (
84
+ <>
85
+ {logo === null ? null : (
86
+ <span className="source-badge__logo">
87
+ <img src={logo} alt="" />
88
+ </span>
89
+ )}
90
+ <span className="source-badge__name">{label}</span>
91
+ {compact || identifier === null ? null : (
92
+ <span className="source-badge__id">{identifier}</span>
93
+ )}
94
+ {count > 1 ? <span className="source-badge__count">{count}</span> : null}
95
+ </>
96
+ );
97
+
98
+ if (href === null) {
99
+ return (
100
+ <span className={className} style={style} title={title}>
101
+ {content}
102
+ </span>
103
+ );
104
+ }
105
+ return (
106
+ <a
107
+ className={className}
108
+ style={style}
109
+ href={href}
110
+ target="_blank"
111
+ rel="noopener noreferrer"
112
+ title={title}
113
+ >
114
+ {content}
115
+ </a>
116
+ );
117
+ }
@@ -0,0 +1,6 @@
1
+ export type { MoleculesSourceBadgeProps } from './MoleculesSourceBadge.tsx';
2
+ export { MoleculesSourceBadge } from './MoleculesSourceBadge.tsx';
3
+ export type { SourceBadgeProps } from './SourceBadge.tsx';
4
+ export { SourceBadge } from './SourceBadge.tsx';
5
+ export type { SourceLooksStore } from './useSourceLooks.ts';
6
+ export { sourceLooksStore, useSourceLooks } from './useSourceLooks.ts';
@@ -0,0 +1,70 @@
1
+ import { useSyncExternalStore } from 'react';
2
+
3
+ import type { SourceLooks } from '../core/moleculesSources.ts';
4
+ import { MOLECULES_URL, fetchSourceLooks } from '../core/moleculesSources.ts';
5
+
6
+ /**
7
+ * Every source's look, read from molecules.cheminfo.org once per page however
8
+ * many badges ask for it.
9
+ * @param url - Where molecules answers.
10
+ * @returns The looks by lower-case name, or null until molecules has answered;
11
+ * an empty map when it could not be read, so the badges stop waiting and stay
12
+ * neutral.
13
+ */
14
+ export function useSourceLooks(
15
+ url: string = MOLECULES_URL,
16
+ ): SourceLooks | null {
17
+ const store = sourceLooksStore(url);
18
+ return useSyncExternalStore(store.subscribe, store.read, notYet);
19
+ }
20
+
21
+ /** The one read of a molecules address, shared by every badge on the page. */
22
+ export interface SourceLooksStore {
23
+ /** Follow the read, starting it on the first call. */
24
+ subscribe: (listener: () => void) => () => void;
25
+ /** What has been read so far. */
26
+ read: () => SourceLooks | null;
27
+ }
28
+
29
+ /**
30
+ * The store behind {@link useSourceLooks}: one per address, read once.
31
+ * @param url - Where molecules answers.
32
+ * @returns The store.
33
+ */
34
+ export function sourceLooksStore(url: string): SourceLooksStore {
35
+ let store = STORES.get(url);
36
+ if (store === undefined) {
37
+ store = createStore(url);
38
+ STORES.set(url, store);
39
+ }
40
+ return store;
41
+ }
42
+
43
+ const STORES = new Map<string, SourceLooksStore>();
44
+
45
+ function createStore(url: string): SourceLooksStore {
46
+ let looks: SourceLooks | null = null;
47
+ let started = false;
48
+ const listeners = new Set<() => void>();
49
+
50
+ function settle(value: SourceLooks): void {
51
+ looks = value;
52
+ for (const listener of listeners) listener();
53
+ }
54
+
55
+ return {
56
+ subscribe(listener) {
57
+ listeners.add(listener);
58
+ if (!started) {
59
+ started = true;
60
+ fetchSourceLooks({ url }).then(settle, () => settle(new Map()));
61
+ }
62
+ return () => listeners.delete(listener);
63
+ },
64
+ read: () => looks,
65
+ };
66
+ }
67
+
68
+ function notYet(): null {
69
+ return null;
70
+ }
@@ -9,3 +9,4 @@ export {
9
9
  checkMoleculesTokens,
10
10
  publicTokenCheck,
11
11
  } from './moleculesToken.ts';
12
+ export { issueTokenCommand, tokensToFix } from './tokenFix.ts';
@@ -0,0 +1,37 @@
1
+ import type {
2
+ MoleculesTokenCheck,
3
+ MoleculesTokenVerdict,
4
+ } from './moleculesToken.ts';
5
+
6
+ /** The verdicts only a new token, written in the server's `.env`, fixes. */
7
+ const NEEDS_NEW_TOKEN: ReadonlySet<MoleculesTokenVerdict> = new Set([
8
+ 'unset',
9
+ 'refused',
10
+ 'otherSource',
11
+ ]);
12
+
13
+ /**
14
+ * The checks whose token has to be replaced: none is set, molecules.cheminfo.org
15
+ * refuses it, or it was issued for another source. A token that could not be
16
+ * checked is not among them, since nothing written in the `.env` fixes that.
17
+ * @param checks - The last checks.
18
+ * @returns Those to fix, in the order given.
19
+ */
20
+ export function tokensToFix(
21
+ checks: readonly MoleculesTokenCheck[],
22
+ ): MoleculesTokenCheck[] {
23
+ return checks.filter((check) => NEEDS_NEW_TOKEN.has(check.verdict));
24
+ }
25
+
26
+ /**
27
+ * The command molecules.cheminfo.org issues a check's token with, on its
28
+ * server. The source is quoted, since a name like `NP Atlas` holds a space;
29
+ * the email is left for the reader to write.
30
+ * @param check - The check, whose first source the token is issued for.
31
+ * @returns The command.
32
+ */
33
+ export function issueTokenCommand(
34
+ check: Pick<MoleculesTokenCheck, 'sources'>,
35
+ ): string {
36
+ return `npm run tokens -- issue '${check.sources[0] ?? 'SOURCE'}' EMAIL`;
37
+ }
@@ -0,0 +1,95 @@
1
+ import { Callout } from '@blueprintjs/core';
2
+ import type { ReactElement } from 'react';
3
+
4
+ import { CodeBlock } from '../../clipboard/ui/CodeBlock.tsx';
5
+ import { useLanguage } from '../../i18n/ui/useLanguage.ts';
6
+ import { useChromeT } from '../../i18n/ui/useT.ts';
7
+ import { trimTrailingSlash } from '../../router/core/address.ts';
8
+ import type { MoleculesTokenCheck } from '../core/moleculesToken.ts';
9
+ import { issueTokenCommand, tokensToFix } from '../core/tokenFix.ts';
10
+
11
+ /** Where molecules.cheminfo.org answers when the site names no other. */
12
+ const DEFAULT_MOLECULES_URL = 'https://molecules.cheminfo.org';
13
+
14
+ /** What {@link MoleculesTokenFix} shows. */
15
+ export interface MoleculesTokenFixProps {
16
+ /** The last checks. */
17
+ checks: readonly MoleculesTokenCheck[];
18
+ /**
19
+ * Where molecules.cheminfo.org answers, whose Admin page issues tokens.
20
+ * @default 'https://molecules.cheminfo.org'
21
+ */
22
+ moleculesUrl?: string;
23
+ }
24
+
25
+ /**
26
+ * What to do about the tokens that have to be replaced — none set, refused, or
27
+ * issued for another source — in the three steps it takes: issue one on
28
+ * molecules.cheminfo.org, write it in the server's `.env`, restart. Nothing
29
+ * when every token is fine.
30
+ * @param props - See {@link MoleculesTokenFixProps}.
31
+ * @returns The steps, or null.
32
+ */
33
+ export function MoleculesTokenFix(
34
+ props: MoleculesTokenFixProps,
35
+ ): ReactElement | null {
36
+ const { checks, moleculesUrl = DEFAULT_MOLECULES_URL } = props;
37
+ const t = useChromeT();
38
+ const language = useLanguage();
39
+ const toFix = tokensToFix(checks);
40
+ if (toFix.length === 0) return null;
41
+
42
+ const one = toFix.length === 1;
43
+ const variables = new Intl.ListFormat(language, {
44
+ type: 'conjunction',
45
+ }).format(toFix.map((check) => check.variable));
46
+ const admin = `${trimTrailingSlash(moleculesUrl)}/admin`;
47
+ const unsetOnly = toFix.every((check) => check.verdict === 'unset');
48
+
49
+ return (
50
+ <Callout
51
+ intent={unsetOnly ? 'warning' : 'danger'}
52
+ icon="key"
53
+ title={t('moleculesToken.fix.title', { variables })}
54
+ className="molecules-tokens__fix"
55
+ data-testid="molecules-tokens-fix"
56
+ >
57
+ <ol>
58
+ <li>
59
+ <p>
60
+ {t(
61
+ one
62
+ ? 'moleculesToken.fix.issueOne'
63
+ : 'moleculesToken.fix.issueMany',
64
+ )}{' '}
65
+ <a href={admin} target="_blank" rel="noreferrer">
66
+ {admin}
67
+ </a>
68
+ </p>
69
+ <p>{t('moleculesToken.fix.issueOnServer')}</p>
70
+ <CodeBlock
71
+ tone="muted"
72
+ copyable
73
+ code={[...new Set(toFix.map(issueTokenCommand))].join('\n')}
74
+ />
75
+ </li>
76
+ <li>
77
+ <p>
78
+ {t(
79
+ one ? 'moleculesToken.fix.envOne' : 'moleculesToken.fix.envMany',
80
+ )}
81
+ </p>
82
+ <CodeBlock
83
+ tone="muted"
84
+ copyable
85
+ code={toFix.map((check) => `${check.variable}=mct_…`).join('\n')}
86
+ />
87
+ </li>
88
+ <li>
89
+ <p>{t('moleculesToken.fix.restart')}</p>
90
+ <CodeBlock tone="muted" copyable code="docker compose up -d" />
91
+ </li>
92
+ </ol>
93
+ </Callout>
94
+ );
95
+ }
@@ -1,16 +1,18 @@
1
1
  import type { Intent } from '@blueprintjs/core';
2
2
  import { Button, Callout, HTMLTable, Tag } from '@blueprintjs/core';
3
- import type { ReactNode } from 'react';
4
3
  import { useMemo } from 'react';
5
4
 
6
5
  import type { ChromeKey } from '../../i18n/core/chromeCatalog.ts';
7
6
  import { useLanguage } from '../../i18n/ui/useLanguage.ts';
8
- import type { Translate } from '../../i18n/ui/useT.ts';
9
7
  import { useChromeT } from '../../i18n/ui/useT.ts';
10
8
  import type {
11
9
  MoleculesTokenCheck,
12
10
  MoleculesTokenVerdict,
13
11
  } from '../core/moleculesToken.ts';
12
+ import { tokensToFix } from '../core/tokenFix.ts';
13
+
14
+ import { MoleculesTokenFix } from './MoleculesTokenFix.tsx';
15
+ import { tokenDetails } from './tokenDetails.tsx';
14
16
 
15
17
  /** What {@link MoleculesTokenTable} shows. */
16
18
  export interface MoleculesTokenTableProps {
@@ -32,6 +34,11 @@ export interface MoleculesTokenTableProps {
32
34
  * @default undefined
33
35
  */
34
36
  error?: string;
37
+ /**
38
+ * Where molecules.cheminfo.org answers, whose Admin page issues tokens.
39
+ * @default 'https://molecules.cheminfo.org'
40
+ */
41
+ moleculesUrl?: string;
35
42
  }
36
43
 
37
44
  const INTENT: Record<MoleculesTokenVerdict, Intent> = {
@@ -60,12 +67,12 @@ const ISSUE_COMMAND = 'npm run tokens -- issue SOURCE EMAIL';
60
67
  * The tokens a site holds for molecules.cheminfo.org, one row each, with what
61
68
  * molecules.cheminfo.org said of it when it was last checked: valid, another
62
69
  * source's, refused, not set. A token is changed in the server's `.env`, never
63
- * here, so the table says where.
70
+ * here, so the table says where, and above it how, when one has to be.
64
71
  * @param props - See {@link MoleculesTokenTableProps}.
65
72
  * @returns The table.
66
73
  */
67
74
  export function MoleculesTokenTable(props: MoleculesTokenTableProps) {
68
- const { checks, pending = false, onCheckAgain, error } = props;
75
+ const { checks, pending = false, onCheckAgain, error, moleculesUrl } = props;
69
76
  const t = useChromeT();
70
77
  const language = useLanguage();
71
78
  const formats = useMemo(
@@ -117,6 +124,9 @@ export function MoleculesTokenTable(props: MoleculesTokenTableProps) {
117
124
  {error}
118
125
  </Callout>
119
126
  )}
127
+ {checks !== null && (
128
+ <MoleculesTokenFix checks={checks} moleculesUrl={moleculesUrl} />
129
+ )}
120
130
  {checks === null ? (
121
131
  <p className="molecules-tokens__muted">
122
132
  {t('moleculesToken.notChecked')}
@@ -148,102 +158,18 @@ export function MoleculesTokenTable(props: MoleculesTokenTableProps) {
148
158
  {t(LABEL[check.verdict])}
149
159
  </Tag>
150
160
  </td>
151
- <td>{details(check, t, formats.instant)}</td>
161
+ <td>{tokenDetails(check, t, formats.instant)}</td>
152
162
  </tr>
153
163
  ))}
154
164
  </tbody>
155
165
  </HTMLTable>
156
166
  </div>
157
167
  )}
158
- <p className="molecules-tokens__muted">
159
- {t('moleculesToken.howToChange')} <code>{ISSUE_COMMAND}</code>
160
- </p>
168
+ {(checks === null || tokensToFix(checks).length === 0) && (
169
+ <p className="molecules-tokens__muted">
170
+ {t('moleculesToken.howToChange')} <code>{ISSUE_COMMAND}</code>
171
+ </p>
172
+ )}
161
173
  </section>
162
174
  );
163
175
  }
164
-
165
- /**
166
- * What a row says besides its verdict.
167
- * @param check - The check.
168
- * @param t - The chrome's translator.
169
- * @param instant - How an instant is written.
170
- * @returns The cell's content.
171
- */
172
- function details(
173
- check: MoleculesTokenCheck,
174
- t: Translate<ChromeKey>,
175
- instant: Intl.DateTimeFormat,
176
- ): ReactNode {
177
- const hint: Fact | null =
178
- check.hint === null
179
- ? null
180
- : ['hint', <code key="hint">{check.hint}…</code>];
181
- switch (check.verdict) {
182
- case 'ok':
183
- return facts([
184
- check.sources.length === 1 || check.source === null
185
- ? null
186
- : ['source', t('moleculesToken.forSource', { source: check.source })],
187
- check.issuedAt === null
188
- ? null
189
- : [
190
- 'issued',
191
- check.email === null
192
- ? t('moleculesToken.issued', {
193
- when: instant.format(check.issuedAt),
194
- })
195
- : t('moleculesToken.issuedTo', {
196
- email: check.email,
197
- when: instant.format(check.issuedAt),
198
- }),
199
- ],
200
- [
201
- 'used',
202
- check.lastUsedAt === null
203
- ? t('moleculesToken.neverUsed')
204
- : t('moleculesToken.lastUsed', {
205
- when: instant.format(check.lastUsedAt),
206
- }),
207
- ],
208
- hint,
209
- ]);
210
- case 'otherSource':
211
- return facts([
212
- [
213
- 'source',
214
- t('moleculesToken.otherSourceDetail', {
215
- source: check.source ?? '',
216
- }),
217
- ],
218
- hint,
219
- ]);
220
- case 'refused':
221
- return check.message ?? t('moleculesToken.refusedDetail');
222
- case 'unset':
223
- return t('moleculesToken.unsetDetail', { variable: check.variable });
224
- case 'uncheckable':
225
- return t('moleculesToken.uncheckableDetail');
226
- case 'unanswered':
227
- return check.message;
228
- default:
229
- return null;
230
- }
231
- }
232
-
233
- /** One fact about a token, and the key it is drawn under. */
234
- type Fact = readonly [key: string, content: ReactNode];
235
-
236
- /**
237
- * Facts about a token, one after the other.
238
- * @param parts - The facts; null ones are left out.
239
- * @returns The facts, separated by a middle dot.
240
- */
241
- function facts(parts: ReadonlyArray<Fact | null>): ReactNode {
242
- const shown = parts.filter((part) => part !== null);
243
- return shown.map(([key, content], index) => (
244
- <span key={key}>
245
- {index > 0 && ' · '}
246
- {content}
247
- </span>
248
- ));
249
- }
@@ -1,2 +1,4 @@
1
+ export type { MoleculesTokenFixProps } from './MoleculesTokenFix.tsx';
2
+ export { MoleculesTokenFix } from './MoleculesTokenFix.tsx';
1
3
  export type { MoleculesTokenTableProps } from './MoleculesTokenTable.tsx';
2
4
  export { MoleculesTokenTable } from './MoleculesTokenTable.tsx';
@@ -0,0 +1,91 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ import type { ChromeKey } from '../../i18n/core/chromeCatalog.ts';
4
+ import type { Translate } from '../../i18n/ui/useT.ts';
5
+ import type { MoleculesTokenCheck } from '../core/moleculesToken.ts';
6
+
7
+ /**
8
+ * What a row says besides its verdict.
9
+ * @param check - The check.
10
+ * @param t - The chrome's translator.
11
+ * @param instant - How an instant is written.
12
+ * @returns The cell's content.
13
+ */
14
+ export function tokenDetails(
15
+ check: MoleculesTokenCheck,
16
+ t: Translate<ChromeKey>,
17
+ instant: Intl.DateTimeFormat,
18
+ ): ReactNode {
19
+ const hint: Fact | null =
20
+ check.hint === null
21
+ ? null
22
+ : ['hint', <code key="hint">{check.hint}…</code>];
23
+ switch (check.verdict) {
24
+ case 'ok':
25
+ return facts([
26
+ check.sources.length === 1 || check.source === null
27
+ ? null
28
+ : ['source', t('moleculesToken.forSource', { source: check.source })],
29
+ check.issuedAt === null
30
+ ? null
31
+ : [
32
+ 'issued',
33
+ check.email === null
34
+ ? t('moleculesToken.issued', {
35
+ when: instant.format(check.issuedAt),
36
+ })
37
+ : t('moleculesToken.issuedTo', {
38
+ email: check.email,
39
+ when: instant.format(check.issuedAt),
40
+ }),
41
+ ],
42
+ [
43
+ 'used',
44
+ check.lastUsedAt === null
45
+ ? t('moleculesToken.neverUsed')
46
+ : t('moleculesToken.lastUsed', {
47
+ when: instant.format(check.lastUsedAt),
48
+ }),
49
+ ],
50
+ hint,
51
+ ]);
52
+ case 'otherSource':
53
+ return facts([
54
+ [
55
+ 'source',
56
+ t('moleculesToken.otherSourceDetail', {
57
+ source: check.source ?? '',
58
+ }),
59
+ ],
60
+ hint,
61
+ ]);
62
+ case 'refused':
63
+ return check.message ?? t('moleculesToken.refusedDetail');
64
+ case 'unset':
65
+ return t('moleculesToken.unsetDetail', { variable: check.variable });
66
+ case 'uncheckable':
67
+ return t('moleculesToken.uncheckableDetail');
68
+ case 'unanswered':
69
+ return check.message;
70
+ default:
71
+ return null;
72
+ }
73
+ }
74
+
75
+ /** One fact about a token, and the key it is drawn under. */
76
+ type Fact = readonly [key: string, content: ReactNode];
77
+
78
+ /**
79
+ * Facts about a token, one after the other.
80
+ * @param parts - The facts; null ones are left out.
81
+ * @returns The facts, separated by a middle dot.
82
+ */
83
+ function facts(parts: ReadonlyArray<Fact | null>): ReactNode {
84
+ const shown = parts.filter((part) => part !== null);
85
+ return shown.map(([key, content], index) => (
86
+ <span key={key}>
87
+ {index > 0 && ' · '}
88
+ {content}
89
+ </span>
90
+ ));
91
+ }
package/src/ui.ts CHANGED
@@ -18,6 +18,7 @@ export * from './help/ui/index.ts';
18
18
  export * from './hooks/ui/index.ts';
19
19
  export * from './i18n/ui/index.ts';
20
20
  export * from './language/ui/index.ts';
21
+ export * from './moleculesSources/ui/index.ts';
21
22
  export * from './moleculesToken/ui/index.ts';
22
23
  export * from './number/ui/index.ts';
23
24
  export * from './overlay/ui/index.ts';