diff --git a/.env b/.env
index f7e8676da..3f175f57a 100644
--- a/.env
+++ b/.env
@@ -20,7 +20,7 @@ LETS_ENCRYPT_PROVIDER=
LETS_ENCRYPT_CA_SERVER=https://acme-staging-v02.api.letsencrypt.org/directory
PHRASEA_DOMAIN="${PHRASEA_DOMAIN:-phrasea.local}"
-DASHBOARD_URL=https://dashboard.${PHRASEA_DOMAIN}${HTTPS_PORT_PREFIX}
+DASHBOARD_CLIENT_URL=https://dashboard.${PHRASEA_DOMAIN}${HTTPS_PORT_PREFIX}
S3_ENDPOINT=https://minio.${PHRASEA_DOMAIN}${HTTPS_PORT_PREFIX}
UPLOADER_API_URL=https://api-uploader.${PHRASEA_DOMAIN}${HTTPS_PORT_PREFIX}
EXPOSE_API_URL=https://api-expose.${PHRASEA_DOMAIN}${HTTPS_PORT_PREFIX}
@@ -51,6 +51,8 @@ APP_ENV=prod
# Enables some features for debugging applications
DEV_MODE=false
+# Dashboard
+DASHBOARD_CLIENT_ID=dashboard-app
DISPLAY_SERVICES_MENU=true
# Minio
diff --git a/configurator/config/services.yaml b/configurator/config/services.yaml
index 743cca60b..703268955 100644
--- a/configurator/config/services.yaml
+++ b/configurator/config/services.yaml
@@ -21,6 +21,7 @@ services:
- databox
- expose
- uploader
+ - dashboard
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
diff --git a/dashboard/client/config-compiler.js b/dashboard/client/config-compiler.js
index 52ee823a5..efe8e8be4 100644
--- a/dashboard/client/config-compiler.js
+++ b/dashboard/client/config-compiler.js
@@ -1,4 +1,15 @@
(function (config, env) {
+ config = config || {};
+
+ const analytics = {};
+
+ if (env.MATOMO_URL) {
+ analytics.matomo = {
+ baseUrl: env.MATOMO_URL,
+ siteId: env.MATOMO_SITE_ID,
+ };
+ }
+
const whiteList = [
'DATABOX_API_URL',
'DATABOX_CLIENT_URL',
@@ -33,8 +44,36 @@
}
});
+ function castBoolean(value) {
+ if (typeof value === 'boolean') {
+ return value;
+ }
+
+ if (typeof value === 'string') {
+ return ['true', '1', 'on', 'y', 'yes'].includes(
+ value.toLowerCase()
+ );
+ }
+
+ return false;
+ }
+
+
return {
locales: config.available_locales,
+ autoConnectIdP: env.AUTO_CONNECT_IDP,
+ baseUrl: env.DASHBOARD_CLIENT_URL,
+ keycloakUrl: env.KEYCLOAK_URL,
+ realmName: env.KEYCLOAK_REALM_NAME,
+ clientId: env.CLIENT_ID,
+ devMode: castBoolean(env.DEV_MODE),
+ displayServicesMenu: castBoolean(env.DISPLAY_SERVICES_MENU),
+ dashboardBaseUrl: env.DASHBOARD_CLIENT_URL,
+ analytics,
+ appId: env.APP_ID || 'dashboard',
+ sentryDsn: env.SENTRY_DSN,
+ sentryEnvironment: env.SENTRY_ENVIRONMENT,
+ sentryRelease: env.SENTRY_RELEASE,
env: e,
};
});
diff --git a/dashboard/client/package.json b/dashboard/client/package.json
index 80dde20b4..c105b7241 100644
--- a/dashboard/client/package.json
+++ b/dashboard/client/package.json
@@ -11,13 +11,19 @@
"preview": "vite preview"
},
"dependencies": {
+ "@alchemy/api": "workspace:*",
+ "@alchemy/auth": "workspace:*",
+ "@alchemy/core": "workspace:*",
+ "@alchemy/react-auth": "workspace:*",
"@alchemy/theme-editor": "workspace:*",
- "@mui/material": "^5.15.0",
+ "@alchemy/phrasea-ui": "workspace:*",
+ "@mui/material": "^5.15.1",
"@mui/icons-material": "^5.15.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-google-font-loader": "^1.1.0",
- "vite-plugin-svgr": "^4.2.0"
+ "vite-plugin-svgr": "^4.2.0",
+ "react-i18next": "^13.5.0"
},
"devDependencies": {
"@types/node": "^18.8.5",
diff --git a/dashboard/client/src/Dashboard.tsx b/dashboard/client/src/Dashboard.tsx
new file mode 100644
index 000000000..d70932cac
--- /dev/null
+++ b/dashboard/client/src/Dashboard.tsx
@@ -0,0 +1,136 @@
+import {Alert, Chip, Container, Grid, Typography, useMediaQuery, useTheme,} from '@mui/material';
+import Service from './Service';
+import ClientApp from './ClientApp.tsx';
+import config from './config.ts';
+import ApiIcon from '@mui/icons-material/Api';
+import SellIcon from '@mui/icons-material/Sell';
+import keycloakImg from './images/keycloak.png';
+import databoxImg from './images/databox.png';
+import uploaderImg from './images/uploader.png';
+import exposeImg from './images/expose.png';
+import notifyImg from './images/notify.png';
+import DashboardBar from "./DashboardBar";
+import {useKeycloakUser} from '@alchemy/react-auth'
+import AdminPanelSettingsIcon from "@mui/icons-material/AdminPanelSettings";
+
+type Props = {};
+
+export default function Dashboard({}: Props) {
+ const theme = useTheme();
+ const isLarge = useMediaQuery(theme.breakpoints.up('sm'));
+ const {user} = useKeycloakUser();
+
+ const {
+ DATABOX_API_URL,
+ EXPOSE_API_URL,
+ UPLOADER_API_URL,
+ NOTIFY_API_URL,
+ DATABOX_CLIENT_URL,
+ EXPOSE_CLIENT_URL,
+ UPLOADER_CLIENT_URL,
+ STACK_NAME,
+ DEV_MODE,
+ STACK_VERSION,
+ } = config.env;
+
+ console.debug('config', config);
+
+ return (
+
+ {isLarge && (
+
+
+ {STACK_NAME}
+ {user && }
+ label={STACK_VERSION}
+ color={'info'}
+ />}
+
+
+ )}
+
+ {user && isLarge && DEV_MODE && (
+
+ Developer Mode is enabled
+
+ )}
+
+
+ ,
+ href: `${config.keycloakUrl}/admin/master/console`,
+ title: `Master Admin`,
+ },
+ ]}
+ />
+ {DATABOX_API_URL && (
+
+ )}
+ {EXPOSE_API_URL && (
+
+ )}
+ {UPLOADER_API_URL && (
+
+ )}
+ {NOTIFY_API_URL && (
+ ,
+ href: NOTIFY_API_URL,
+ title: `API documentation of Notify`,
+ },
+ ]}
+ />
+ )}
+
+
+ );
+}
diff --git a/dashboard/client/src/DashboardBar.tsx b/dashboard/client/src/DashboardBar.tsx
new file mode 100644
index 000000000..e079b9b3c
--- /dev/null
+++ b/dashboard/client/src/DashboardBar.tsx
@@ -0,0 +1,53 @@
+import AppBar from '@mui/material/AppBar';
+import Toolbar from '@mui/material/Toolbar';
+import {PropsWithChildren} from "react";
+import {useKeycloakUrls, useKeycloakUser} from '@alchemy/react-auth';
+import config from "./config.ts";
+import {keycloakClient} from "./lib/apiClient.ts";
+import MenuItem from "@mui/material/MenuItem";
+import Box from "@mui/material/Box";
+import {UserMenu} from '@alchemy/phrasea-ui';
+import {useTranslation} from "react-i18next";
+
+type Props = PropsWithChildren<{}>;
+
+export default function DashboardBar({
+ children
+}: Props) {
+ const menuHeight = 42;
+ const {t} = useTranslation();
+ const {getLoginUrl, getAccountUrl} = useKeycloakUrls({
+ autoConnectIdP: config.autoConnectIdP,
+ keycloakClient,
+ });
+
+ const {user, logout} = useKeycloakUser();
+
+ return (
+
+
+
+ {children}
+
+
+
+
+ {!user ? (
+
+ ) : (
+
+ )}
+
+
+
+ );
+}
diff --git a/dashboard/client/src/Root.tsx b/dashboard/client/src/Root.tsx
index 3c9f981ae..26d981b0e 100644
--- a/dashboard/client/src/Root.tsx
+++ b/dashboard/client/src/Root.tsx
@@ -1,118 +1,38 @@
-import {Alert, Chip, Container, Grid, Typography, useMediaQuery, useTheme} from '@mui/material';
-import Service from './Service';
-import ClientApp from './ClientApp.tsx';
-import config from './config.ts';
-import ApiIcon from "@mui/icons-material/Api";
-import SellIcon from '@mui/icons-material/Sell';
-import keycloakImg from './images/keycloak.png'
-import databoxImg from './images/databox.png'
-import uploaderImg from './images/uploader.png'
-import exposeImg from './images/expose.png'
-import notifyImg from './images/notify.png'
+import {oauthClient} from './lib/apiClient';
+import {AuthenticationProvider, SessionExpireContainer, useAuthorizationCode} from '@alchemy/react-auth';
+import {FullPageLoader} from '@alchemy/phrasea-ui';
+import Dashboard from "./Dashboard.tsx";
type Props = {};
export default function Root({}: Props) {
const {
- DATABOX_API_URL,
- EXPOSE_API_URL,
- UPLOADER_API_URL,
- NOTIFY_API_URL,
- KEYCLOAK_URL,
- DATABOX_CLIENT_URL,
- EXPOSE_CLIENT_URL,
- UPLOADER_CLIENT_URL,
- STACK_NAME,
- DEV_MODE,
- STACK_VERSION,
- } = config.env;
+ error,
+ hasCode,
+ } = useAuthorizationCode({
+ oauthClient,
+ allowNoCode: true,
+ navigate: (path, {replace} = {}) => {
+ if (replace) {
+ document.location.replace(path);
+ } else {
+ document.location.href = path
+ }
+ },
+ successUri: '/'
+ });
- console.debug('config.env', config.env);
-
- const theme = useTheme();
- const isLarge = useMediaQuery(theme.breakpoints.up('sm'));
+ if (error) {
+ return
@@ -75,7 +74,7 @@ export default function Service({
display: {
xs: 'none',
md: 'block',
- }
+ },
}}
>
{description}
diff --git a/dashboard/client/src/config.ts b/dashboard/client/src/config.ts
index 5812f2328..3c6cb8674 100644
--- a/dashboard/client/src/config.ts
+++ b/dashboard/client/src/config.ts
@@ -1,7 +1,8 @@
+import {WindowConfig} from '@alchemy/core';
+
declare global {
interface Window {
config: {
- locales: string[];
env: {
DATABOX_API_URL: string;
DATABOX_CLIENT_URL: string;
@@ -10,7 +11,6 @@ declare global {
ELASTICHQ_URL: string;
EXPOSE_API_URL: string;
EXPOSE_CLIENT_URL: string;
- KEYCLOAK_URL: string;
MAILHOG_URL: string;
MATOMO_URL: string;
NOTIFY_API_URL: string;
@@ -27,10 +27,12 @@ declare global {
UPLOADER_CLIENT_URL: string;
ZIPPY_URL: string;
};
- };
+ } & WindowConfig;
}
}
const config = window.config;
+config.appName = 'dashboard';
+
export default config;
diff --git a/dashboard/client/src/index.tsx b/dashboard/client/src/index.tsx
index 2c02dce19..e4f905997 100644
--- a/dashboard/client/src/index.tsx
+++ b/dashboard/client/src/index.tsx
@@ -1,16 +1,15 @@
import ReactDOM from 'react-dom/client';
-import Root from './Root.tsx';
import React from 'react';
import {CssBaseline, GlobalStyles, responsiveFontSizes} from '@mui/material';
import {ThemeEditorProvider} from '@alchemy/theme-editor';
-import {scrollbarWidth, theme} from "./theme.ts";
+import {scrollbarWidth, theme} from './theme.ts';
+import Root from "./Root.tsx";
ReactDOM.createRoot(document.getElementById('root')!).render(
responsiveFontSizes(theme, {
- })}
+ transformTheme={theme => responsiveFontSizes(theme, {})}
>
-
+
);
diff --git a/dashboard/client/src/lib/apiClient.ts b/dashboard/client/src/lib/apiClient.ts
new file mode 100644
index 000000000..f094a7bc0
--- /dev/null
+++ b/dashboard/client/src/lib/apiClient.ts
@@ -0,0 +1,17 @@
+import {configureClientAuthentication, KeycloakClient} from '@alchemy/auth';
+import {createHttpClient} from '@alchemy/api';
+
+import config from '../config';
+
+export const keycloakClient = new KeycloakClient({
+ clientId: config.clientId,
+ baseUrl: config.keycloakUrl,
+ realm: config.realmName,
+});
+export const oauthClient = keycloakClient.client;
+
+const apiClient = createHttpClient(window.config.baseUrl);
+
+configureClientAuthentication(apiClient, oauthClient);
+
+export default apiClient;
diff --git a/dashboard/client/src/theme.ts b/dashboard/client/src/theme.ts
index 776e664e5..da43a49bd 100644
--- a/dashboard/client/src/theme.ts
+++ b/dashboard/client/src/theme.ts
@@ -1,10 +1,10 @@
-import {ThemeOptions} from "@mui/material";
+import {ThemeOptions} from '@mui/material';
export const theme: ThemeOptions = {
typography: {
fontFamily: "'Montserrat', sans-serif",
h1: {
- fontSize: '3rem',
+ fontSize: '2rem',
fontWeight: 600,
},
h2: {
diff --git a/databox/api/migrations/Version20231221125408.php b/databox/api/migrations/Version20231221125408.php
new file mode 100644
index 000000000..01c6d6638
--- /dev/null
+++ b/databox/api/migrations/Version20231221125408.php
@@ -0,0 +1,34 @@
+addSql('ALTER TABLE rendition_definition ADD key VARCHAR(150) DEFAULT NULL');
+ $this->addSql('CREATE UNIQUE INDEX uniq_rend_def_ws_key ON rendition_definition (workspace_id, key)');
+ }
+
+ public function down(Schema $schema): void
+ {
+ // this down() migration is auto-generated, please modify it to your needs
+ $this->addSql('CREATE SCHEMA public');
+ $this->addSql('DROP INDEX uniq_rend_def_ws_key');
+ $this->addSql('ALTER TABLE rendition_definition DROP key');
+ }
+}
diff --git a/databox/api/src/Api/InputTransformer/RenditionDefinitionInputTransformer.php b/databox/api/src/Api/InputTransformer/RenditionDefinitionInputTransformer.php
new file mode 100644
index 000000000..e49e1121d
--- /dev/null
+++ b/databox/api/src/Api/InputTransformer/RenditionDefinitionInputTransformer.php
@@ -0,0 +1,92 @@
+workspace) {
+ $workspace = $data->workspace;
+ }
+
+ if ($isNew) {
+ if (!$workspace instanceof Workspace) {
+ throw new BadRequestHttpException('Missing workspace');
+ }
+
+ if ($data->key) {
+ $rendDef = $this->em->getRepository(RenditionDefinition::class)
+ ->findOneBy([
+ 'key' => $data->key,
+ 'workspace' => $workspace->getId()
+ ]);
+
+ if ($rendDef) {
+ $isNew = false;
+ $object = $rendDef;
+ }
+ }
+ }
+
+ if ($isNew) {
+ $object->setWorkspace($workspace);
+ $object->setKey($data->key);
+ }
+
+ if (null !== $data->name) {
+ $object->setName($data->name);
+ }
+ if (null !== $data->class) {
+ $object->setClass($data->class);
+ }
+
+ if (null !== $data->download) {
+ $object->setDownload($data->download);
+ }
+ if (null !== $data->pickSourceFile) {
+ $object->setPickSourceFile($data->pickSourceFile);
+ }
+ if (null !== $data->useAsOriginal) {
+ $object->setUseAsOriginal($data->useAsOriginal);
+ }
+ if (null !== $data->useAsPreview) {
+ $object->setUseAsPreview($data->useAsPreview);
+ }
+ if (null !== $data->useAsThumbnail) {
+ $object->setUseAsThumbnail($data->useAsThumbnail);
+ }
+ if (null !== $data->useAsThumbnailActive) {
+ $object->setUseAsThumbnailActive($data->useAsThumbnailActive);
+ }
+ if (null !== $data->definition) {
+ $object->setDefinition($data->definition);
+ }
+ if (null !== $data->priority) {
+ $object->setPriority($data->priority);
+ }
+
+ return $object;
+ }
+}
diff --git a/databox/api/src/Api/Model/Input/RenditionDefinitionInput.php b/databox/api/src/Api/Model/Input/RenditionDefinitionInput.php
new file mode 100644
index 000000000..cc851bf08
--- /dev/null
+++ b/databox/api/src/Api/Model/Input/RenditionDefinitionInput.php
@@ -0,0 +1,85 @@
+ [RenditionDefinition::GROUP_WRITE],
],
+ input: RenditionDefinitionInput::class,
order: ['priority' => 'DESC'],
provider: RenditionDefinitionCollectionProvider::class,
)]
#[ORM\Table]
#[ORM\Index(columns: ['workspace_id', 'name'], name: 'rend_def_ws_name')]
+#[ORM\UniqueConstraint(name: 'uniq_rend_def_ws_key', columns: ['workspace_id', 'key'])]
#[ORM\Entity]
class RenditionDefinition extends AbstractUuidEntity implements \Stringable
{
@@ -90,6 +93,12 @@ class RenditionDefinition extends AbstractUuidEntity implements \Stringable
#[Groups(['_'])]
protected ?Workspace $workspace = null;
+ /**
+ * Unique key by workspace. Used to prevent duplicates.
+ */
+ #[ORM\Column(type: Types::STRING, length: 150, nullable: true)]
+ private ?string $key = null;
+
#[Groups([RenditionDefinition::GROUP_LIST, RenditionDefinition::GROUP_READ, RenditionDefinition::GROUP_WRITE])]
#[ORM\Column(type: Types::STRING, length: 80)]
private ?string $name = null;
@@ -255,4 +264,14 @@ public function setPickSourceFile(bool $pickSourceFile): void
{
$this->pickSourceFile = $pickSourceFile;
}
+
+ public function getKey(): ?string
+ {
+ return $this->key;
+ }
+
+ public function setKey(?string $key): void
+ {
+ $this->key = $key;
+ }
}
diff --git a/databox/api/src/Workspace/WorkspaceDuplicateManager.php b/databox/api/src/Workspace/WorkspaceDuplicateManager.php
index af335f39e..6d659e67f 100644
--- a/databox/api/src/Workspace/WorkspaceDuplicateManager.php
+++ b/databox/api/src/Workspace/WorkspaceDuplicateManager.php
@@ -62,6 +62,7 @@ private function copyRenditionDefinitions(Workspace $from, Workspace $to): void
$i->setWorkspace($to);
$i->setClass($classMap[$item->getClass()->getId()]);
$i->setPriority($item->getPriority());
+ $i->setKey($item->getKey());
$i->setUseAsOriginal($item->isUseAsOriginal());
$i->setUseAsPreview($item->isUseAsPreview());
$i->setUseAsThumbnail($item->isUseAsThumbnail());
diff --git a/databox/client/config-compiler.js b/databox/client/config-compiler.js
index 50ea74c68..15d4e2518 100644
--- a/databox/client/config-compiler.js
+++ b/databox/client/config-compiler.js
@@ -61,7 +61,7 @@
devMode: castBoolean(env.DEV_MODE),
requestSignatureTtl: env.S3_REQUEST_SIGNATURE_TTL,
displayServicesMenu: castBoolean(env.DISPLAY_SERVICES_MENU),
- dashboardBaseUrl: env.DASHBOARD_URL,
+ dashboardBaseUrl: env.DASHBOARD_CLIENT_URL,
allowedTypes: normalizeTypes(env.ALLOWED_FILE_TYPES),
analytics,
appId: env.APP_ID || 'databox',
diff --git a/databox/client/package.json b/databox/client/package.json
index 042e2e49e..a498ca061 100644
--- a/databox/client/package.json
+++ b/databox/client/package.json
@@ -11,6 +11,7 @@
"@alchemy/theme-editor": "workspace:*",
"@alchemy/visual-workflow": "workspace:*",
"@alchemy/react-hooks": "workspace:*",
+ "@alchemy/phrasea-ui": "workspace:*",
"@alchemy/navigation": "workspace:*",
"@dnd-kit/core": "^6.0.5",
"@dnd-kit/sortable": "^7.0.1",
@@ -18,8 +19,8 @@
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.15.0",
- "@mui/lab": "^5.0.0-alpha.156",
- "@mui/material": "^5.15.0",
+ "@mui/lab": "^5.0.0-alpha.157",
+ "@mui/material": "^5.15.1",
"@toast-ui/react-image-editor": "^3.15.2",
"ace-builds": "^1.14.0",
"axios": "^1.6.2",
diff --git a/databox/client/src/components/App.tsx b/databox/client/src/components/App.tsx
index 008824faa..7d2333c3e 100644
--- a/databox/client/src/components/App.tsx
+++ b/databox/client/src/components/App.tsx
@@ -83,8 +83,11 @@ const AppProxy = React.memo(() => {
export default function App() {
const {logout} = useAuth();
const onError = useRequestErrorHandler({
- logout: (redirectPathAfterLogin) => {
- logout(redirectPathAfterLogin, true);
+ logout: redirectPathAfterLogin => {
+ logout({
+ redirectPathAfterLogin,
+ quiet: true,
+ });
},
});
diff --git a/databox/client/src/components/Dialog/Collection/EditCollection.tsx b/databox/client/src/components/Dialog/Collection/EditCollection.tsx
index 535c965bb..63a693d23 100644
--- a/databox/client/src/components/Dialog/Collection/EditCollection.tsx
+++ b/databox/client/src/components/Dialog/Collection/EditCollection.tsx
@@ -37,11 +37,7 @@ export default function EditCollection({data, onClose, minHeight}: Props) {
},
});
- const {
- submitting,
- remoteErrors,
- forbidNavigation,
- } = usedFormSubmit;
+ const {submitting, remoteErrors, forbidNavigation} = usedFormSubmit;
useInRouterDirtyFormPrompt(t, forbidNavigation);
const formId = 'edit-collection';
diff --git a/databox/client/src/components/Dialog/Tabbed/FormTab.tsx b/databox/client/src/components/Dialog/Tabbed/FormTab.tsx
index e5f176ff7..12e54aa28 100644
--- a/databox/client/src/components/Dialog/Tabbed/FormTab.tsx
+++ b/databox/client/src/components/Dialog/Tabbed/FormTab.tsx
@@ -6,7 +6,10 @@ import {LoadingButton} from '@mui/lab';
import SaveIcon from '@mui/icons-material/Save';
import RemoteErrors from '../../Form/RemoteErrors';
import {useTranslation} from 'react-i18next';
-import {useInRouterDirtyFormPrompt, useOutsideRouterDirtyFormPrompt} from '@alchemy/navigation';
+import {
+ useInRouterDirtyFormPrompt,
+ useOutsideRouterDirtyFormPrompt,
+} from '@alchemy/navigation';
type Props = PropsWithChildren<{
loading: boolean;
diff --git a/databox/client/src/components/Dialog/Tabbed/TabbedDialog.tsx b/databox/client/src/components/Dialog/Tabbed/TabbedDialog.tsx
index c0ae39191..f9c689413 100644
--- a/databox/client/src/components/Dialog/Tabbed/TabbedDialog.tsx
+++ b/databox/client/src/components/Dialog/Tabbed/TabbedDialog.tsx
@@ -87,12 +87,13 @@ export default function TabbedDialog({
);
})}
- {currentTab && React.createElement(currentTab.component, {
- ...rest,
- ...currentTab.props,
- onClose: closeModal,
- minHeight,
- })}
+ {currentTab &&
+ React.createElement(currentTab.component, {
+ ...rest,
+ ...currentTab.props,
+ onClose: closeModal,
+ minHeight,
+ })}
)}
diff --git a/databox/client/src/components/Dialog/Workspace/DefinitionManager.tsx b/databox/client/src/components/Dialog/Workspace/DefinitionManager.tsx
index c1b8dfe24..3b9283528 100644
--- a/databox/client/src/components/Dialog/Workspace/DefinitionManager.tsx
+++ b/databox/client/src/components/Dialog/Workspace/DefinitionManager.tsx
@@ -186,12 +186,7 @@ export default function DefinitionManager({
},
});
- const {
- submitting,
- remoteErrors,
- forbidNavigation,
- reset,
- } = usedFormSubmit;
+ const {submitting, remoteErrors, forbidNavigation, reset} = usedFormSubmit;
React.useEffect(() => {
if (item && 'new' !== item) {
diff --git a/databox/client/src/components/Dialog/Workspace/RenditionDefinitionManager.tsx b/databox/client/src/components/Dialog/Workspace/RenditionDefinitionManager.tsx
index f51220374..d42375af5 100644
--- a/databox/client/src/components/Dialog/Workspace/RenditionDefinitionManager.tsx
+++ b/databox/client/src/components/Dialog/Workspace/RenditionDefinitionManager.tsx
@@ -18,7 +18,7 @@ import RenditionClassSelect from '../../Form/RenditionClassSelect';
import CheckboxWidget from '../../Form/CheckboxWidget';
import apiClient from '../../../api/api-client';
import {toast} from 'react-toastify';
-import React from "react";
+import React from 'react';
function Item({
data,
diff --git a/databox/client/src/components/Dialog/Workspace/WorkspaceDialog.tsx b/databox/client/src/components/Dialog/Workspace/WorkspaceDialog.tsx
index b7c0661fb..a6bd3ebf8 100644
--- a/databox/client/src/components/Dialog/Workspace/WorkspaceDialog.tsx
+++ b/databox/client/src/components/Dialog/Workspace/WorkspaceDialog.tsx
@@ -15,9 +15,6 @@ import RenditionClassManager from './RenditionClassManager';
import RenditionDefinitionManager from './RenditionDefinitionManager';
import InfoWorkspace from './InfoWorkspace';
import {modalRoutes} from '../../../routes.ts';
-import {useForceLogin} from '@alchemy/react-auth';
-import config from "../../../config.ts";
-import {keycloakClient} from "../../../api/api-client.ts";
type Props = {};
@@ -27,11 +24,6 @@ export default function WorkspaceDialog({}: Props) {
const [data, setData] = useState();
- useForceLogin({
- keycloakClient,
- autoConnectIdP: config.autoConnectIdP,
- });
-
useEffect(() => {
getWorkspace(id!).then(c => setData(c));
}, [id]);
diff --git a/databox/client/src/components/Layout/ChangeTheme.tsx b/databox/client/src/components/Layout/ChangeTheme.tsx
index 81d122a65..f8f84d47f 100644
--- a/databox/client/src/components/Layout/ChangeTheme.tsx
+++ b/databox/client/src/components/Layout/ChangeTheme.tsx
@@ -15,9 +15,7 @@ import {StackedModalProps, useModals} from '@alchemy/navigation';
type Props = {} & StackedModalProps;
-export default function ChangeTheme({
- open,
-}: Props) {
+export default function ChangeTheme({open}: Props) {
const {t} = useTranslation();
const prefContext = useContext(UserPreferencesContext);
const {preferences, updatePreference} = prefContext;
diff --git a/databox/client/src/components/Layout/MainAppBar.tsx b/databox/client/src/components/Layout/MainAppBar.tsx
index bcbe0baf7..3ceac1a8e 100644
--- a/databox/client/src/components/Layout/MainAppBar.tsx
+++ b/databox/client/src/components/Layout/MainAppBar.tsx
@@ -1,22 +1,16 @@
-import * as React from 'react';
import {useContext} from 'react';
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import Toolbar from '@mui/material/Toolbar';
import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
-import Menu from '@mui/material/Menu';
import MenuIcon from '@mui/icons-material/Menu';
import Container from '@mui/material/Container';
-import Avatar from '@mui/material/Avatar';
-import Tooltip from '@mui/material/Tooltip';
import MenuItem from '@mui/material/MenuItem';
import {useTranslation} from 'react-i18next';
-import {Divider, ListItemIcon, ListItemText} from '@mui/material';
-import LogoutIcon from '@mui/icons-material/Logout';
+import {ListItemIcon, ListItemText} from '@mui/material';
import {SearchContext} from '../Media/Search/SearchContext';
import ColorLensIcon from '@mui/icons-material/ColorLens';
-import AccountBoxIcon from '@mui/icons-material/AccountBox';
import {zIndex} from '../../themes/zIndex';
import {useKeycloakUrls} from '@alchemy/react-auth';
import {ThemeEditorContext} from '@alchemy/theme-editor';
@@ -25,8 +19,9 @@ import {keycloakClient} from '../../api/api-client.ts';
import {useUser} from '../../lib/auth.ts';
import {DashboardMenu} from '@alchemy/react-ps';
import {useModals} from '@alchemy/navigation';
-import ChangeTheme from "./ChangeTheme.tsx";
-import ThemeEditor from "./ThemeEditor.tsx";
+import ChangeTheme from './ChangeTheme.tsx';
+import ThemeEditor from './ThemeEditor.tsx';
+import {UserMenu} from '@alchemy/phrasea-ui';
export const menuHeight = 42;
@@ -39,10 +34,7 @@ export default function MainAppBar({onToggleLeftPanel}: Props) {
const {t} = useTranslation();
const {openModal} = useModals();
const themeEditorContext = useContext(ThemeEditorContext);
- const userContext = useUser();
- const [anchorElUser, setAnchorElUser] = React.useState(
- null
- );
+ const {user, logout} = useUser();
const searchContext = useContext(SearchContext);
const {getAccountUrl, getLoginUrl} = useKeycloakUrls({
keycloakClient,
@@ -51,15 +43,7 @@ export default function MainAppBar({onToggleLeftPanel}: Props) {
const onTitleClick = () =>
searchContext.selectWorkspace(undefined, undefined, true);
- const handleOpenUserMenu = (event: React.MouseEvent) => {
- setAnchorElUser(event.currentTarget);
- };
-
- const handleCloseUserMenu = () => {
- setAnchorElUser(null);
- };
-
- const username = userContext.user?.username;
+ const username = user?.username;
return (
) : (
- <>
-
-
-
- {(
- username[0] || 'U'
- ).toUpperCase()}
-
-
-
-
- >
+ ]}
+ />
)}
{config.displayServicesMenu && (
-
+
)}
diff --git a/databox/client/src/components/Layout/ThemeEditor.tsx b/databox/client/src/components/Layout/ThemeEditor.tsx
index 837c700b9..b652f5ae0 100644
--- a/databox/client/src/components/Layout/ThemeEditor.tsx
+++ b/databox/client/src/components/Layout/ThemeEditor.tsx
@@ -1,24 +1,17 @@
-import AppDialog from "./AppDialog.tsx";
+import AppDialog from './AppDialog.tsx';
import {StackedModalProps, useModals} from '@alchemy/navigation';
import {MuiThemeEditor} from '@alchemy/theme-editor';
type Props = {} & StackedModalProps;
-export default function ThemeEditor({
- open,
- modalIndex,
-}: Props) {
+export default function ThemeEditor({open, modalIndex}: Props) {
const {closeModal} = useModals();
- const onClose= () => closeModal();
+ const onClose = () => closeModal();
- return
-
-
+ return (
+
+
+
+ );
}
diff --git a/databox/client/src/components/Media/Asset/AssetContextMenu.tsx b/databox/client/src/components/Media/Asset/AssetContextMenu.tsx
index 5b210b420..802f01b53 100644
--- a/databox/client/src/components/Media/Asset/AssetContextMenu.tsx
+++ b/databox/client/src/components/Media/Asset/AssetContextMenu.tsx
@@ -22,7 +22,7 @@ import SaveAsButton from './Actions/SaveAsButton';
import {useNavigateToModal} from '../../Routing/ModalLink';
import SaveIcon from '@mui/icons-material/Save';
import {modalRoutes} from '../../../routes.ts';
-import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
+import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
type Props = {
anchorPosition: PopoverPosition;
diff --git a/databox/client/src/components/Media/Collection/CreateCollection.tsx b/databox/client/src/components/Media/Collection/CreateCollection.tsx
index 958390a8a..98fb143e2 100644
--- a/databox/client/src/components/Media/Collection/CreateCollection.tsx
+++ b/databox/client/src/components/Media/Collection/CreateCollection.tsx
@@ -9,7 +9,7 @@ import {CollectionChip, WorkspaceChip} from '../../Ui/Chips';
import {StackedModalProps, useModals} from '@alchemy/navigation';
import {OnCollectionEdit} from '../../Dialog/Collection/EditCollection';
import React from 'react';
-import {useDirtyFormPromptOutsideRouter} from "../../Dialog/Tabbed/FormTab.tsx";
+import {useDirtyFormPromptOutsideRouter} from '../../Dialog/Tabbed/FormTab.tsx';
type Props = {
parent?: string;
@@ -58,11 +58,7 @@ export default function CreateCollection({
},
});
- const {
- submitting,
- remoteErrors,
- forbidNavigation,
- } = usedFormSubmit;
+ const {submitting, remoteErrors, forbidNavigation} = usedFormSubmit;
useDirtyFormPromptOutsideRouter(forbidNavigation);
const formId = 'create-collection';
@@ -96,10 +92,7 @@ export default function CreateCollection({
errors={remoteErrors}
open={open}
>
-
+
);
}
diff --git a/databox/client/src/components/Media/Search/SelectionActions.tsx b/databox/client/src/components/Media/Search/SelectionActions.tsx
index 8db24c604..3ac206565 100644
--- a/databox/client/src/components/Media/Search/SelectionActions.tsx
+++ b/databox/client/src/components/Media/Search/SelectionActions.tsx
@@ -268,32 +268,37 @@ export default function SelectionActions({layout, onLayoutChange}: Props) {
: t('asset_actions.select_all', 'Select all')
}
>
-
-
+
-
+
+
+
-
-
- {children}
-
- >
+ return (
+ <>
+
+ {children}
+ >
+ );
}
diff --git a/databox/client/src/components/Routing/RouteProxy.tsx b/databox/client/src/components/Routing/RouteProxy.tsx
index a63f909ce..a05f80cc2 100644
--- a/databox/client/src/components/Routing/RouteProxy.tsx
+++ b/databox/client/src/components/Routing/RouteProxy.tsx
@@ -16,8 +16,8 @@ export default function RouteProxy({
if (!isPublic && !isAuthenticated()) {
document.location.href = getLoginUrl();
- return null
+ return null;
}
- return
+ return ;
}
diff --git a/databox/client/src/components/Upload/UploadModal.tsx b/databox/client/src/components/Upload/UploadModal.tsx
index f5e8abcae..77cc63d5e 100644
--- a/databox/client/src/components/Upload/UploadModal.tsx
+++ b/databox/client/src/components/Upload/UploadModal.tsx
@@ -23,7 +23,8 @@ import {
import {getBatchActions} from '../Media/Asset/Attribute/BatchActions';
import {
StackedModalProps,
- useModals, useOutsideRouterDirtyFormPrompt,
+ useModals,
+ useOutsideRouterDirtyFormPrompt,
} from '@alchemy/navigation';
import {Privacy} from '../../api/privacy.ts';
import {Asset} from '../../types.ts';
diff --git a/databox/client/src/components/User/Preferences/UserPreferencesProvider.tsx b/databox/client/src/components/User/Preferences/UserPreferencesProvider.tsx
index 7976f367f..99953a9fc 100644
--- a/databox/client/src/components/User/Preferences/UserPreferencesProvider.tsx
+++ b/databox/client/src/components/User/Preferences/UserPreferencesProvider.tsx
@@ -8,7 +8,7 @@ import {
import {getUserPreferences, putUserPreferences} from '../../../api/user';
import {createCachedThemeOptions} from '../../../lib/theme';
import {CssBaseline, GlobalStyles} from '@mui/material';
-import {useAuth} from '@alchemy/react-auth';
+import {useKeycloakUser} from '@alchemy/react-auth';
import {ThemeEditorProvider} from '@alchemy/theme-editor';
const sessionStorageKey = 'userPrefs';
@@ -29,7 +29,7 @@ export default function UserPreferencesProvider({children}: Props) {
const [preferences, setPreferences] = React.useState(
getFromStorage()
);
- const {tokens} = useAuth();
+ const {user} = useKeycloakUser();
const updatePreference = React.useCallback(
(name, value) => {
@@ -42,7 +42,7 @@ export default function UserPreferencesProvider({children}: Props) {
newPrefs[name] = value;
}
- if (tokens) {
+ if (user) {
putUserPreferences(name, newPrefs[name]);
}
@@ -54,18 +54,18 @@ export default function UserPreferencesProvider({children}: Props) {
return newPrefs;
});
},
- [tokens]
+ [user]
);
React.useEffect(() => {
- if (tokens) {
+ if (user) {
getUserPreferences().then(r =>
setPreferences({
...r,
})
);
}
- }, [tokens]);
+ }, [user?.id]);
const value = React.useMemo(() => {
return {
@@ -79,7 +79,9 @@ export default function UserPreferencesProvider({children}: Props) {
return (
-
+
);
diff --git a/databox/client/src/lib/theme.ts b/databox/client/src/lib/theme.ts
index f2e00e470..4a41dc58b 100644
--- a/databox/client/src/lib/theme.ts
+++ b/databox/client/src/lib/theme.ts
@@ -12,5 +12,9 @@ export function createCachedThemeOptions(name: ThemeName): ThemeOptions {
return themeCache[name];
}
- return (themeCache[name] = mergeDeep({}, baseTheme, themes[name]) as ThemeOptions);
+ return (themeCache[name] = mergeDeep(
+ {},
+ baseTheme,
+ themes[name]
+ ) as ThemeOptions);
}
diff --git a/databox/indexer/package.json b/databox/indexer/package.json
index abd6034e4..045be7800 100644
--- a/databox/indexer/package.json
+++ b/databox/indexer/package.json
@@ -10,7 +10,8 @@
"scripts": {
"console": "node dist/console.mjs",
"dev": "nodemon",
- "build": "rimraf ./dist && vite build",
+ "validate": "tsc -p ./tsconfig.check.json",
+ "build": "pnpm validate && rimraf ./dist && vite build",
"test": "jest",
"sync-databox-types": "generate-api-platform-client --generator typescript http://databox-api src/",
"format": "prettier --ignore-path .gitignore --write \"**/*.+(js|ts|json|cjs|tsx|jsx)\"",
diff --git a/databox/indexer/src/alternateUrl.ts b/databox/indexer/src/alternateUrl.ts
index 759fba95f..e5c9f4a1b 100644
--- a/databox/indexer/src/alternateUrl.ts
+++ b/databox/indexer/src/alternateUrl.ts
@@ -18,8 +18,8 @@ export function getAlternateUrls(
return alternateUrls.map((c): AlternateUrl => {
return {
type: c.name,
- url: c.pathPattern.replace(/\${(.+)}/g, (_m, m1) => {
- return dict[m1];
+ url: c.pathPattern.replace(/\${(.+)}/g, (_m, m1: string) => {
+ return dict[m1 as keyof typeof dict] as string;
}),
};
});
diff --git a/databox/indexer/src/command/commandUtil.ts b/databox/indexer/src/command/commandUtil.ts
new file mode 100644
index 000000000..813a58f49
--- /dev/null
+++ b/databox/indexer/src/command/commandUtil.ts
@@ -0,0 +1,8 @@
+import {setLogLevel} from "../lib/logger";
+import {CommandCommonOptions} from "../types";
+
+export function applyCommonOptions(opts: O): void {
+ if (opts.debug) {
+ setLogLevel('debug');
+ }
+}
diff --git a/databox/indexer/src/command/index.ts b/databox/indexer/src/command/index.ts
index c6b01db98..f9a3b1f38 100644
--- a/databox/indexer/src/command/index.ts
+++ b/databox/indexer/src/command/index.ts
@@ -3,16 +3,19 @@ import {createLogger} from '../lib/logger.js';
import {indexers} from '../indexers.js';
import {getLocation} from '../locations.js';
import {consume} from '../databox/entrypoint.js';
-import {runServer} from "../server";
+import {runServer} from '../server';
+import {CommandCommonOptions} from "../types";
+import {applyCommonOptions} from "./commandUtil";
export type IndexOptions = {
createNewWorkspace?: boolean;
-};
+} & CommandCommonOptions;
export default async function indexCommand(
locationName: string,
options: IndexOptions
) {
+ applyCommonOptions(options);
const location = getLocation(locationName);
const databoxLogger = createLogger('databox');
diff --git a/databox/indexer/src/command/watch.ts b/databox/indexer/src/command/watch.ts
index c20685300..b13f132c9 100644
--- a/databox/indexer/src/command/watch.ts
+++ b/databox/indexer/src/command/watch.ts
@@ -1,15 +1,16 @@
import {createDataboxClientFromConfig} from '../databox/client.js';
import {createLogger} from '../lib/logger.js';
-import {runServer} from "../server";
-import {IndexLocation} from "../types/config";
-import {getConfig} from "../configLoader";
-import {watchers} from "../watchers";
+import {runServer} from '../server';
+import {IndexLocation} from '../types/config';
+import {getConfig} from '../configLoader';
+import {watchers} from '../watchers';
+import {CommandCommonOptions} from "../types";
+import {applyCommonOptions} from "./commandUtil";
-export type WatchOptions = {
-};
+export type WatchOptions = {} & CommandCommonOptions;
export default async function watchCommand(options: WatchOptions) {
-
+ applyCommonOptions(options);
const mainLogger = createLogger('app');
const databoxLogger = createLogger('databox');
@@ -28,5 +29,4 @@ export default async function watchCommand(options: WatchOptions) {
});
runServer(mainLogger);
-
}
diff --git a/databox/indexer/src/configLoader.ts b/databox/indexer/src/configLoader.ts
index 1f409b56f..4acee3f11 100644
--- a/databox/indexer/src/configLoader.ts
+++ b/databox/indexer/src/configLoader.ts
@@ -10,7 +10,7 @@ function loadConfig(): object {
}
function replaceEnv(str: string): string | boolean | number | undefined {
- let transform;
+ let transform: string | undefined;
let hasEnv = false;
let result: string | undefined = str.replace(
/%env\(([^^)]+)\)%/g,
@@ -62,8 +62,8 @@ function parseConfig(config: any): any {
if (Array.isArray(config)) {
return config.map(parseConfig);
} else {
- const sub = {};
- Object.keys(config).forEach(k => {
+ const sub: Record = {};
+ Object.keys(config).forEach((k: string) => {
sub[k] = parseConfig(config[k]);
});
return sub;
@@ -84,10 +84,11 @@ export function getConfig(
let p = root;
for (let i = 0; i < parts.length; ++i) {
- const k = parts[i];
+ const k = parts[i] as string;
if (!p.hasOwnProperty(k)) {
return defaultValue;
}
+ // @ts-expect-error any
p = p[parts[i]];
}
diff --git a/databox/indexer/src/console.ts b/databox/indexer/src/console.ts
index 571f27770..020e8c2e5 100644
--- a/databox/indexer/src/console.ts
+++ b/databox/indexer/src/console.ts
@@ -1,14 +1,14 @@
-import {Command} from 'commander';
+import {Command, Option} from 'commander';
import indexCommand from './command/index.js';
-import listCommand from "./command/list";
-import watchCommand from "./command/watch";
+import listCommand from './command/list';
+import watchCommand from './command/watch';
const program = new Command();
-program
- .name('indexer')
- .description('Databox Indexer')
- .version('1.0.0');
+program.name('console').description('Databox Indexer').version('1.0.0');
+
+const debugOption = new Option('--debug', 'Debug mode')
+ .default(false);
program
.command('index')
@@ -19,21 +19,16 @@ program
'Remove existing workspace and create a new empty one',
false
)
+ .addOption(debugOption)
.action(indexCommand);
program
.command('watch')
.description('Watch locations')
- .option(
- '-l, --location',
- 'List locations to watch',
- false
- )
+ .option('-l, --location', 'List locations to watch', false)
+ .addOption(debugOption)
.action(watchCommand);
-program
- .command('list')
- .description('List locations')
- .action(listCommand);
+program.command('list').description('List locations').action(listCommand);
program.parse();
diff --git a/databox/indexer/src/databox/client.ts b/databox/indexer/src/databox/client.ts
index 593e5dd3c..02728b1bd 100644
--- a/databox/indexer/src/databox/client.ts
+++ b/databox/indexer/src/databox/client.ts
@@ -191,7 +191,7 @@ export class DataboxClient {
return r;
}
- async createRenditionClass(data): Promise {
+ async createRenditionClass(data: object): Promise {
const res = await this.client.post(`/rendition-classes`, data);
return res.data.id;
@@ -207,7 +207,7 @@ export class DataboxClient {
return res.data['hydra:member'];
}
- async createRenditionDefinition(data): Promise {
+ async createRenditionDefinition(data: object): Promise {
await this.client.post(`/rendition-definitions`, data);
}
diff --git a/databox/indexer/src/handlers/fs/shared.ts b/databox/indexer/src/handlers/fs/shared.ts
index 6d95855a7..96ca0c74c 100644
--- a/databox/indexer/src/handlers/fs/shared.ts
+++ b/databox/indexer/src/handlers/fs/shared.ts
@@ -29,7 +29,10 @@ export function createAsset(
const p = dirPrefix ? dirPrefix + relativePath : path;
const sourcePath = sourceDir ? sourceDir + relativePath : path;
- console.log('generatePublicUrl(p, locationName)', generatePublicUrl(p, locationName));
+ console.log(
+ 'generatePublicUrl(p, locationName)',
+ generatePublicUrl(p, locationName)
+ );
return {
workspaceId,
diff --git a/databox/indexer/src/handlers/phraseanet/indexer.ts b/databox/indexer/src/handlers/phraseanet/indexer.ts
index b5089b8b0..719c88ae2 100644
--- a/databox/indexer/src/handlers/phraseanet/indexer.ts
+++ b/databox/indexer/src/handlers/phraseanet/indexer.ts
@@ -59,15 +59,17 @@ export const phraseanetIndexer: IndexIterator =
await client.getMetaStruct(dm.databoxId)
);
for (const m of metaStructure) {
- logger.debug(`Creating "${m.name}" attribute definition`);
+ logger.info(`Creating "${m.name}" attribute definition`);
const id = m.id.toString();
if (!attrClassIndex[defaultPublicClass]) {
+ const name = 'Phraseanet Public';
+ logger.info(`Creating "${name}" attribute class`);
attrClassIndex[defaultPublicClass] =
await databoxClient.createAttributeClass(
defaultPublicClass,
{
- name: 'Phraseanet Public',
+ name,
public: true,
editable: true,
workspace: `/workspaces/${workspaceId}`,
@@ -104,7 +106,7 @@ export const phraseanetIndexer: IndexIterator =
const subDefs = await client.getSubDefinitions(dm.databoxId);
for (const sd of subDefs) {
if (!classIndex[sd.class]) {
- logger.debug(`Creating rendition class "${sd.class}" `);
+ logger.info(`Creating rendition class "${sd.class}" `);
classIndex[sd.class] =
await databoxClient.createRenditionClass({
name: sd.class,
@@ -117,6 +119,7 @@ export const phraseanetIndexer: IndexIterator =
);
await databoxClient.createRenditionDefinition({
name: sd.name,
+ key: `${sd.name}_${sd.type ?? ''}`,
class: `/rendition-classes/${classIndex[sd.class]}`,
useAsOriginal: sd.name === 'document',
useAsPreview: sd.name === 'preview',
diff --git a/databox/indexer/src/handlers/phraseanet/shared.ts b/databox/indexer/src/handlers/phraseanet/shared.ts
index 18e3a7124..e0800de01 100644
--- a/databox/indexer/src/handlers/phraseanet/shared.ts
+++ b/databox/indexer/src/handlers/phraseanet/shared.ts
@@ -7,7 +7,7 @@ import {
RenditionInput,
} from '../../databox/types';
-const renditionDefinitionMapping = {
+const renditionDefinitionMapping: Record = {
document: 'original',
};
const renditionDefinitionBlacklist = ['original'];
@@ -82,6 +82,6 @@ export function createAsset(
};
}
-export const attributeTypesEquivalence = {
+export const attributeTypesEquivalence: Record = {
string: 'text',
};
diff --git a/databox/indexer/src/lib/axios.ts b/databox/indexer/src/lib/axios.ts
index 23e23855c..b84be7fcb 100644
--- a/databox/indexer/src/lib/axios.ts
+++ b/databox/indexer/src/lib/axios.ts
@@ -87,8 +87,13 @@ export function createHttpClient({
};
}
+ logger.debug(
+ `Error response headers (${error.config?.url}): ` +
+ JSON.stringify(error.response.headers, undefined, 2)
+ );
logger.error(
- `Error response (${error.config.url}): ` + JSON.stringify(filtered, undefined, 2)
+ `Error response (${error.config?.url}): ` +
+ JSON.stringify(filtered, undefined, 2)
);
}
@@ -96,14 +101,22 @@ export function createHttpClient({
}
);
+ const obfuscate = (str: string) => str.replace(/([\w._-]{5})[\w._-]{30,}/g, '$1***');
+
client.interceptors.request.use(
- (config) => {
- logger.debug(`${config.method.toUpperCase()} ${config.url}
-${JSON.stringify(config.headers)}${config.data ? `\n${JSON.stringify(config.data)}` : ''}`);
+ config => {
+ if (!config) {
+ return config;
+ }
+
+ logger.debug(`${config.method?.toUpperCase()} ${config.url}
+${obfuscate(JSON.stringify(config.headers, null, 2))}${
+ config.data ? `\n${obfuscate(JSON.stringify(config.data, null, 2))}` : ''
+ }`);
return config;
},
- (error) => {
+ error => {
return Promise.reject(error);
}
);
diff --git a/databox/indexer/src/lib/logger.ts b/databox/indexer/src/lib/logger.ts
index ebbc97bf5..41adeca1c 100644
--- a/databox/indexer/src/lib/logger.ts
+++ b/databox/indexer/src/lib/logger.ts
@@ -10,11 +10,31 @@ const myFormat = printf(({context, level, message, timestamp}) => {
return `${timestamp} ${context}.${level.toUpperCase()}: ${message}`;
});
+type LogLevel = "debug" | "warn" | "info" | "error";
+
+const loggerConfig: {
+ level: LogLevel;
+} = {
+ level: 'info'
+};
+
+const loggers: Logger[] = [];
+
+export function setLogLevel(level: LogLevel): void {
+ loggerConfig.level = level;
+
+ loggers.forEach(l => l.level = level);
+}
+
export function createLogger(context: string): Logger {
- return winstonCreateLogger({
- level: 'debug',
+ const l = winstonCreateLogger({
+ level: loggerConfig.level,
format: combine(timestamp(), myFormat),
defaultMeta: {context},
transports: [new transports.Console()],
});
+
+ loggers.push(l);
+
+ return l;
}
diff --git a/databox/indexer/src/types.ts b/databox/indexer/src/types.ts
new file mode 100644
index 000000000..2e15e5133
--- /dev/null
+++ b/databox/indexer/src/types.ts
@@ -0,0 +1,3 @@
+export type CommandCommonOptions = {
+ debug: boolean;
+}
diff --git a/databox/indexer/tsconfig.check.json b/databox/indexer/tsconfig.check.json
new file mode 100644
index 000000000..d5db6522d
--- /dev/null
+++ b/databox/indexer/tsconfig.check.json
@@ -0,0 +1,30 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "allowJs": true,
+
+ "types": [
+ "node"
+ ]
+ },
+ "include": [
+ "src/*"
+ ]
+}
diff --git a/docker-compose.yml b/docker-compose.yml
index e9e966992..ac2e97faf 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -83,7 +83,7 @@ services:
- KEYCLOAK_REALM_NAME
- MATOMO_HOST
- DISPLAY_SERVICES_MENU
- - DASHBOARD_URL
+ - DASHBOARD_CLIENT_URL
- AUTO_CONNECT_IDP
- MATOMO_URL
- SENTRY_DSN=${CLIENT_SENTRY_DSN}
@@ -121,7 +121,7 @@ services:
- RABBITMQ_SSL
- REPORT_API_URL
- DISPLAY_SERVICES_MENU
- - DASHBOARD_URL
+ - DASHBOARD_CLIENT_URL
- NEWRELIC_ENABLED
- NEWRELIC_LICENSE_KEY
- SENTRY_DSN=${PHP_SENTRY_DSN}
@@ -634,6 +634,10 @@ services:
- UPLOADER_API_URL
- UPLOADER_CLIENT_URL
- ZIPPY_URL
+ - SENTRY_DSN
+ - SENTRY_ENVIRONMENT
+ - SENTRY_RELEASE
+ - CLIENT_ID=${DASHBOARD_CLIENT_ID}
labels:
- "traefik.http.routers.dashboard.rule=Host(`dashboard.${PHRASEA_DOMAIN}`)"
@@ -972,9 +976,11 @@ services:
- DATABOX_CLIENT_ID
- EXPOSE_CLIENT_ID
- UPLOADER_CLIENT_ID
+ - DASHBOARD_CLIENT_ID
- DATABOX_CLIENT_URL
- EXPOSE_CLIENT_URL
- UPLOADER_CLIENT_URL
+ - DASHBOARD_CLIENT_URL=${DASHBOARD_CLIENT_URL}
- POSTGRES_USER
- POSTGRES_PASSWORD
- POSTGRES_HOST=db
diff --git a/expose/api/src/ConfigurationManager.php b/expose/api/src/ConfigurationManager.php
index 261feea78..f044e233f 100644
--- a/expose/api/src/ConfigurationManager.php
+++ b/expose/api/src/ConfigurationManager.php
@@ -32,7 +32,7 @@ class ConfigurationManager
],
'dashboardBaseUrl' => [
'overridableInAdmin' => false,
- 'name' => 'DASHBOARD_URL',
+ 'name' => 'DASHBOARD_CLIENT_URL',
'type' => 'string',
],
'mapBoxToken' => [
diff --git a/expose/client/src/component/Root.tsx b/expose/client/src/component/Root.tsx
index 9c9aace0f..0036828ce 100644
--- a/expose/client/src/component/Root.tsx
+++ b/expose/client/src/component/Root.tsx
@@ -1,6 +1,6 @@
import {ModalStack} from '@alchemy/navigation';
import {oauthClient} from '../lib/api-client';
-import {AuthenticationProvider, MatomoUser} from '@alchemy/react-auth';
+import {AuthenticationProvider, MatomoUser, SessionExpireContainer} from '@alchemy/react-auth';
import App from './App.tsx';
import {ToastContainer} from 'react-toastify';
@@ -10,11 +10,10 @@ export default function Root({}: Props) {
return (
<>
-
+
+
diff --git a/expose/client/src/component/RouteProxy.tsx b/expose/client/src/component/RouteProxy.tsx
index 3a208b113..70f743604 100644
--- a/expose/client/src/component/RouteProxy.tsx
+++ b/expose/client/src/component/RouteProxy.tsx
@@ -1,7 +1,7 @@
import type {RouteProxyProps} from '@alchemy/navigation';
import {useAuth, useKeycloakUrls} from '@alchemy/react-auth';
import config from '../config.ts';
-import {keycloakClient} from "../lib/api-client.ts";
+import {keycloakClient} from '../lib/api-client.ts';
export default function RouteProxy({
component: Component,
@@ -16,8 +16,8 @@ export default function RouteProxy({
if (!isPublic && !isAuthenticated()) {
document.location.href = getLoginUrl();
- return null
+ return null;
}
- return
+ return ;
}
diff --git a/expose/client/src/component/security/AuthenticationMethod.tsx b/expose/client/src/component/security/AuthenticationMethod.tsx
index 469b5aecd..ac7ad55ec 100644
--- a/expose/client/src/component/security/AuthenticationMethod.tsx
+++ b/expose/client/src/component/security/AuthenticationMethod.tsx
@@ -13,7 +13,7 @@ export default function AuthenticationMethod({}: Props) {
const {getLoginUrl} = useKeycloakUrls({
keycloakClient: keycloakClient,
autoConnectIdP: config.autoConnectIdP,
- })
+ });
const onConnect = React.useCallback(() => {
setRedirectPath && setRedirectPath(getCurrentPath());
diff --git a/expose/client/src/config.ts b/expose/client/src/config.ts
index a0868c88d..0b3aba42e 100644
--- a/expose/client/src/config.ts
+++ b/expose/client/src/config.ts
@@ -1,4 +1,4 @@
-import {WindowConfig} from '@alchemy/core'
+import {WindowConfig} from '@alchemy/core';
declare global {
interface Window {
diff --git a/expose/client/src/index.tsx b/expose/client/src/index.tsx
index fc7d8b092..105c3f41a 100644
--- a/expose/client/src/index.tsx
+++ b/expose/client/src/index.tsx
@@ -4,8 +4,8 @@ import ConfigWrapper from './component/ConfigWrapper';
import './i18n/i18n';
import AnalyticsProvider from './component/anaytics/AnalyticsProvider';
import React from 'react';
-import config from "./config";
-import {initSentry} from '@alchemy/core'
+import config from './config';
+import {initSentry} from '@alchemy/core';
initSentry(config);
diff --git a/lib/js/auth/src/client/OAuthClient.ts b/lib/js/auth/src/client/OAuthClient.ts
index f04e66930..0824fa2b9 100644
--- a/lib/js/auth/src/client/OAuthClient.ts
+++ b/lib/js/auth/src/client/OAuthClient.ts
@@ -172,10 +172,7 @@ export default class OAuthClient {
return;
}
- const index = this.listeners[event].findIndex(({h}) => h === callback);
- if (index >= 0) {
- delete this.listeners[event][index];
- }
+ this.listeners[event] = this.listeners[event]!.filter(({h}) => h !== callback);
}
public async getTokenFromAuthCode(code: string, redirectUri: string): Promise {
@@ -329,7 +326,11 @@ export default class OAuthClient {
const t = this.storage.getItem(this.tokenStorageKey);
if (t) {
- return this.tokensCache = JSON.parse(t) as AuthTokens;
+ const tokens = JSON.parse(t) as AuthTokens;
+
+ this.handleSessionTimeout(tokens)
+
+ return this.tokensCache = tokens;
}
}
diff --git a/lib/js/liform-react/.gitignore b/lib/js/liform-react/.gitignore
new file mode 100644
index 000000000..62b82089f
--- /dev/null
+++ b/lib/js/liform-react/.gitignore
@@ -0,0 +1,12 @@
+node_modules
+npm-debug.log
+dist
+lib
+es
+.DS_Store
+yarn.lock
+.nyc_output/
+coverage/
+examples/bundle*
+package-lock.json
+built_docs/
diff --git a/lib/js/liform-react/package.json b/lib/js/liform-react/package.json
new file mode 100644
index 000000000..2fe35d4bd
--- /dev/null
+++ b/lib/js/liform-react/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "@alchemy/liform-react",
+ "version": "1.0.0",
+ "description": "Generate forms from json-schema to use with React (and redux-form)",
+ "main": "./src/index.jsx",
+ "scripts": {},
+ "keywords": [
+ "react",
+ "json-schema",
+ "form",
+ "redux-form"
+ ],
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/limenius/liform-react.git"
+ },
+ "author": "Nacho Martin",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0"
+ },
+ "dependencies": {
+ "ajv": "^8.12.0",
+ "classnames": "^2.2.5",
+ "deepmerge": "^2.0.1",
+ "lodash": "^4.17.21",
+ "prop-types": "^15.5.10",
+ "react-redux": "^9.0.4",
+ "redux": "^4.2.1",
+ "redux-form": "^8.3.10"
+ },
+ "devDependencies": {
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0"
+ }
+}
diff --git a/lib/js/liform-react/src/Form.jsx b/lib/js/liform-react/src/Form.jsx
new file mode 100644
index 000000000..e69de29bb
diff --git a/lib/js/liform-react/src/buildSyncValidation.js b/lib/js/liform-react/src/buildSyncValidation.js
new file mode 100644
index 000000000..187d3f6d0
--- /dev/null
+++ b/lib/js/liform-react/src/buildSyncValidation.js
@@ -0,0 +1,84 @@
+import Ajv from "ajv";
+import merge from "deepmerge";
+import { set as _set } from "lodash";
+
+const setError = (error, schema) => {
+ // convert property accessor (.xxx[].xxx) notation to jsonPointers notation
+ if (error.instancePath.charAt(0) === ".") {
+ error.instancePath = error.instancePath.replace(/[.[]/gi, "/");
+ error.instancePath = error.instancePath.replace(/[\]]/gi, "");
+ }
+ const instancePathParts = error.instancePath.split("/").slice(1);
+ let instancePath = error.instancePath.slice(1).replace(/\//g, ".");
+ const type = findTypeInSchema(schema, instancePathParts);
+
+ let errorToSet;
+ if (type === "array" || type === "allOf" || type === "oneOf") {
+ errorToSet = { _error: error.message };
+ } else {
+ errorToSet = error.message;
+ }
+
+ let errors = {};
+ _set(errors, instancePath, errorToSet);
+ return errors;
+};
+
+const findTypeInSchema = (schema, instancePath) => {
+ if (!schema) {
+ return;
+ } else if (instancePath.length === 0 && schema.hasOwnProperty("type")) {
+ return schema.type;
+ } else {
+ if (schema.type === "array") {
+ return findTypeInSchema(schema.items, instancePath.slice(1));
+ } else if (schema.hasOwnProperty("allOf")) {
+ if (instancePath.length === 0) return "allOf";
+ schema = { ...schema, ...merge.all(schema.allOf) };
+ delete schema.allOf;
+ return findTypeInSchema(schema, instancePath);
+ } else if (schema.hasOwnProperty("oneOf")) {
+ if (instancePath.length === 0) return "oneOf";
+ schema.oneOf.forEach(item => {
+ let type = findTypeInSchema(item, instancePath);
+ if (type) {
+ return type;
+ }
+ });
+ } else {
+ return findTypeInSchema(
+ schema.properties[instancePath[0]],
+ instancePath.slice(1)
+ );
+ }
+ }
+};
+
+const buildSyncValidation = (schema, ajvParam = null) => {
+ let ajv = ajvParam;
+ if (ajv === null) {
+ ajv = new Ajv({
+ allErrors: true,
+ strict: false
+ });
+ }
+ return values => {
+ const valid = ajv.validate(schema, values);
+ if (valid) {
+ return {};
+ }
+ const ajvErrors = ajv.errors;
+
+ let errors = ajvErrors.map(error => {
+ return setError(error, schema);
+ });
+ // We need at least two elements
+ errors.push({});
+ errors.push({});
+ return merge.all(errors);
+ };
+};
+
+export default buildSyncValidation;
+
+export { setError };
diff --git a/lib/js/liform-react/src/compileSchema.js b/lib/js/liform-react/src/compileSchema.js
new file mode 100644
index 000000000..89becd4d1
--- /dev/null
+++ b/lib/js/liform-react/src/compileSchema.js
@@ -0,0 +1,44 @@
+function isObject(thing) {
+ return typeof thing === "object" && thing !== null && !Array.isArray(thing);
+}
+
+function compileSchema(schema, root) {
+ if (!root) {
+ root = schema;
+ }
+ let newSchema;
+
+ if (isObject(schema)) {
+ newSchema = {};
+ for (let i in schema) {
+ if (schema.hasOwnProperty(i)) {
+ if (i === "$ref") {
+ newSchema = compileSchema(resolveRef(schema[i], root), root);
+ } else {
+ newSchema[i] = compileSchema(schema[i], root);
+ }
+ }
+ }
+ return newSchema;
+ }
+
+ if (Array.isArray(schema)) {
+ newSchema = [];
+ for (let i = 0; i < schema.length; i += 1) {
+ newSchema[i] = compileSchema(schema[i], root);
+ }
+ return newSchema;
+ }
+
+ return schema;
+}
+
+function resolveRef(uri, schema) {
+ uri = uri.replace("#/", "");
+ const tokens = uri.split("/");
+ const tip = tokens.reduce((obj, token) => obj[token], schema);
+
+ return tip;
+}
+
+export default compileSchema;
diff --git a/lib/js/liform-react/src/index.jsx b/lib/js/liform-react/src/index.jsx
new file mode 100644
index 000000000..c05d4a7bd
--- /dev/null
+++ b/lib/js/liform-react/src/index.jsx
@@ -0,0 +1,68 @@
+import React from "react";
+import PropTypes from "prop-types";
+import DefaultTheme from "./themes/bootstrap3";
+import { reduxForm } from "redux-form";
+import renderFields from "./renderFields";
+import renderField from "./renderField";
+import processSubmitErrors from "./processSubmitErrors";
+import buildSyncValidation from "./buildSyncValidation";
+import { setError } from "./buildSyncValidation";
+import compileSchema from "./compileSchema";
+
+const BaseForm = props => {
+ const { schema, handleSubmit, theme, error, submitting, context } = props;
+ return (
+
+ );
+};
+
+const Liform = props => {
+ const schema = compileSchema(props.schema);
+ props.schema.showLabel = false;
+ const schemaWithOptions = compileSchema(props.schema);
+ const formName = props.formKey || props.schema.title || "form";
+
+ const FinalForm = reduxForm({
+ form: props.formKey || props.schema.title || "form",
+ validate: props.syncValidation || buildSyncValidation(schema, props.ajv),
+ initialValues: props.initialValues,
+ context: { ...props.context, formName }
+ })(props.baseForm || BaseForm);
+
+ return (
+
+ );
+};
+
+Liform.propTypes = {
+ schema: PropTypes.object,
+ onSubmit: PropTypes.func,
+ initialValues: PropTypes.object,
+ syncValidation: PropTypes.func,
+ formKey: PropTypes.string,
+ baseForm: PropTypes.func,
+ context: PropTypes.object,
+ ajv: PropTypes.object
+};
+
+export default Liform;
+
+export {
+ renderFields,
+ renderField,
+ processSubmitErrors,
+ DefaultTheme,
+ setError,
+ buildSyncValidation,
+ compileSchema,
+};
diff --git a/lib/js/liform-react/src/processSubmitErrors.js b/lib/js/liform-react/src/processSubmitErrors.js
new file mode 100644
index 000000000..74f0ab95d
--- /dev/null
+++ b/lib/js/liform-react/src/processSubmitErrors.js
@@ -0,0 +1,40 @@
+import { SubmissionError } from "redux-form";
+import { isEmpty as _isEmpty } from "lodash"; // added for empty check
+
+const convertToReduxFormErrors = obj => {
+ let objectWithoutChildrenAndFalseErrors = {};
+ Object.keys(obj).map(name => {
+ if (name === "children") {
+ objectWithoutChildrenAndFalseErrors = {
+ ...objectWithoutChildrenAndFalseErrors,
+ ...convertToReduxFormErrors(obj[name])
+ };
+ } else {
+ if (obj[name].hasOwnProperty("children")) {
+ // if children, take field from it and set them directly as own field
+ objectWithoutChildrenAndFalseErrors[name] = convertToReduxFormErrors(
+ obj[name]
+ );
+ } else {
+ if (
+ obj[name].hasOwnProperty("errors") &&
+ !_isEmpty(obj[name]["errors"])
+ ) {
+ // using lodash for empty error check, dont add them if empty
+ objectWithoutChildrenAndFalseErrors[name] = obj[name]["errors"];
+ }
+ }
+ }
+ return null;
+ });
+ return objectWithoutChildrenAndFalseErrors;
+};
+
+const processSubmitErrors = errors => {
+ if (errors.hasOwnProperty("errors")) {
+ errors = convertToReduxFormErrors(errors.errors);
+ throw new SubmissionError(errors);
+ }
+};
+
+export default processSubmitErrors;
diff --git a/lib/js/liform-react/src/renderField.js b/lib/js/liform-react/src/renderField.js
new file mode 100644
index 000000000..2e531301d
--- /dev/null
+++ b/lib/js/liform-react/src/renderField.js
@@ -0,0 +1,51 @@
+import React from "react";
+import deepmerge from "deepmerge";
+
+const guessWidget = (fieldSchema, theme) => {
+ if (fieldSchema.widget) {
+ return fieldSchema.widget;
+ } else if (fieldSchema.hasOwnProperty("enum")) {
+ return "choice";
+ } else if (fieldSchema.hasOwnProperty("oneOf")) {
+ return "oneOf";
+ } else if (theme[fieldSchema.format]) {
+ return fieldSchema.format;
+ }
+ return fieldSchema.type || "object";
+};
+
+const renderField = (
+ fieldSchema,
+ fieldName,
+ theme,
+ prefix = "",
+ context = {},
+ required = false
+) => {
+ if (fieldSchema.hasOwnProperty("allOf")) {
+ fieldSchema = { ...fieldSchema, ...deepmerge.all(fieldSchema.allOf) };
+ delete fieldSchema.allOf;
+ }
+
+ const widget = guessWidget(fieldSchema, theme);
+
+ if (!theme[widget]) {
+ throw new Error("liform: " + widget + " is not defined in the theme");
+ }
+
+ const newFieldName = prefix ? prefix + fieldName : fieldName;
+
+ return React.createElement(theme[widget], {
+ key: fieldName,
+ fieldName: widget === "oneOf" ? fieldName : newFieldName,
+ label:
+ fieldSchema.showLabel === false ? "" : fieldSchema.title || fieldName,
+ required: required,
+ schema: fieldSchema,
+ theme,
+ context,
+ prefix
+ });
+};
+
+export default renderField;
diff --git a/lib/js/liform-react/src/renderFields.js b/lib/js/liform-react/src/renderFields.js
new file mode 100644
index 000000000..ee25342fb
--- /dev/null
+++ b/lib/js/liform-react/src/renderFields.js
@@ -0,0 +1,38 @@
+import renderField from "./renderField";
+
+export const isRequired = (schema, fieldName) => {
+ if (!schema.required) {
+ return false;
+ }
+ return schema.required.indexOf(fieldName) !== -1;
+};
+
+const renderFields = (schema, theme, prefix = null, context = {}) => {
+ let props = [];
+ for (let i in schema.properties) {
+ props.push({ prop: i, propertyOrder: schema.properties[i].propertyOrder });
+ }
+ props = props.sort((a, b) => {
+ if (a.propertyOrder > b.propertyOrder) {
+ return 1;
+ } else if (a.propertyOrder < b.propertyOrder) {
+ return -1;
+ } else {
+ return 0;
+ }
+ });
+ return props.map(item => {
+ const name = item.prop;
+ const field = schema.properties[name];
+ return renderField(
+ field,
+ name,
+ theme,
+ prefix,
+ context,
+ isRequired(schema, name)
+ );
+ });
+};
+
+export default renderFields;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/ArrayWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/ArrayWidget.jsx
new file mode 100644
index 000000000..6e2afd87a
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/ArrayWidget.jsx
@@ -0,0 +1,152 @@
+import React from "react";
+import PropTypes from "prop-types";
+import renderField from "../../renderField";
+import { FieldArray } from "redux-form";
+import { times as _times} from "lodash";
+import ChoiceWidget from "./ChoiceWidget";
+import classNames from "classnames";
+
+const renderArrayFields = (
+ count,
+ schema,
+ theme,
+ fieldName,
+ remove,
+ context,
+ swap
+) => {
+ const prefix = fieldName + ".";
+ if (count) {
+ return _times(count, idx => {
+ return (
+
+
+ {idx !== count - 1 && count > 1 ? (
+ {
+ e.preventDefault();
+ swap(idx, idx + 1);
+ }}
+ >
+
+
+ ) : (
+ ""
+ )}
+ {idx !== 0 && count > 1 ? (
+ {
+ e.preventDefault();
+ swap(idx, idx - 1);
+ }}
+ >
+
+
+ ) : (
+ ""
+ )}
+
+ {
+ e.preventDefault();
+ remove(idx);
+ }}
+ >
+
+
+
+ {renderField(
+ { ...schema, showLabel: false },
+ idx.toString(),
+ theme,
+ prefix,
+ context
+ )}
+
+ );
+ });
+ } else {
+ return null;
+ }
+};
+
+const renderInput = field => {
+ const className = classNames([
+ "arrayType",
+ { "has-error": field.meta.submitFailed && field.meta.error }
+ ]);
+
+ return (
+
+
+ {field.meta.submitFailed &&
+ field.meta.error && (
+
{field.meta.error}
+ )}
+ {renderArrayFields(
+ field.fields.length,
+ field.schema.items,
+ field.theme,
+ field.fieldName,
+ idx => field.fields.remove(idx),
+ field.context,
+ (a, b) => {
+ field.fields.swap(a, b);
+ }
+ )}
+
field.fields.push()}
+ >
+ Add
+
+
+
+ );
+};
+
+const CollectionWidget = props => {
+ return (
+
+ );
+};
+
+const ArrayWidget = props => {
+ // Arrays are tricky because they can be multiselects or collections
+ if (
+ props.schema.items.hasOwnProperty("enum") &&
+ props.schema.hasOwnProperty("uniqueItems") &&
+ props.schema.uniqueItems
+ ) {
+ return ChoiceWidget({
+ ...props,
+ schema: props.schema.items,
+ multiple: true
+ });
+ } else {
+ return CollectionWidget(props);
+ }
+};
+
+ArrayWidget.propTypes = {
+ schema: PropTypes.object.isRequired,
+ fieldName: PropTypes.string,
+ label: PropTypes.string,
+ theme: PropTypes.object,
+ context: PropTypes.object
+};
+
+export default ArrayWidget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/BaseInputWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/BaseInputWidget.jsx
new file mode 100644
index 000000000..fd7861409
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/BaseInputWidget.jsx
@@ -0,0 +1,59 @@
+import React from "react";
+import PropTypes from "prop-types";
+import classNames from "classnames";
+import { Field } from "redux-form";
+
+const renderInput = field => {
+ const className = classNames([
+ "form-group",
+ { "has-error": field.meta.touched && field.meta.error }
+ ]);
+ return (
+
+
+
+ {field.meta.touched &&
+ field.meta.error && (
+ {field.meta.error}
+ )}
+ {field.description && (
+ {field.description}
+ )}
+
+ );
+};
+
+const BaseInputWidget = props => {
+ return (
+
+ );
+};
+
+BaseInputWidget.propTypes = {
+ schema: PropTypes.object.isRequired,
+ type: PropTypes.string.isRequired,
+ required: PropTypes.bool,
+ fieldName: PropTypes.string,
+ label: PropTypes.string,
+ normalizer: PropTypes.func
+};
+
+export default BaseInputWidget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/CheckboxWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/CheckboxWidget.jsx
new file mode 100644
index 000000000..aadcf10cc
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/CheckboxWidget.jsx
@@ -0,0 +1,56 @@
+import React from "react";
+import PropTypes from "prop-types";
+import classNames from "classnames";
+import { Field } from "redux-form";
+
+const renderInput = field => {
+ const className = classNames([
+ "form-group",
+ { "has-error": field.meta.touched && field.meta.error }
+ ]);
+ return (
+
+
+
+
+ {field.meta.touched &&
+ field.meta.error && (
+
{field.meta.error}
+ )}
+ {field.description && (
+
{field.description}
+ )}
+
+ );
+};
+
+const CheckboxWidget = props => {
+ return (
+
+ );
+};
+
+CheckboxWidget.propTypes = {
+ schema: PropTypes.object.isRequired,
+ fieldName: PropTypes.string,
+ label: PropTypes.string,
+ theme: PropTypes.object
+};
+
+export default CheckboxWidget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/ChoiceExpandedWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/ChoiceExpandedWidget.jsx
new file mode 100644
index 000000000..71b776036
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/ChoiceExpandedWidget.jsx
@@ -0,0 +1,67 @@
+import React from "react";
+import classNames from "classnames";
+import { Field } from "redux-form";
+
+const zipObject = (props, values) =>
+ props.reduce(
+ (prev, prop, i) => Object.assign(prev, { [prop]: values[i] }),
+ {}
+ );
+
+const renderChoice = field => {
+ const className = classNames([
+ "form-group",
+ { "has-error": field.meta.touched && field.meta.error }
+ ]);
+ const options = field.schema.enum;
+ const optionNames = field.schema.enum_titles || options;
+
+ const selectOptions = zipObject(options, optionNames);
+ return (
+
+
+ {Object.entries(selectOptions).map(([value, name]) => (
+
+
+
+ ))}
+
+ {field.meta.touched &&
+ field.meta.error && (
+
{field.meta.error}
+ )}
+ {field.description && (
+
{field.description}
+ )}
+
+ );
+};
+
+const ChoiceExpandedWidget = props => {
+ return (
+
+ );
+};
+
+export default ChoiceExpandedWidget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/ChoiceMultipleExpandedWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/ChoiceMultipleExpandedWidget.jsx
new file mode 100644
index 000000000..f4099150e
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/ChoiceMultipleExpandedWidget.jsx
@@ -0,0 +1,84 @@
+import React from "react";
+import classNames from "classnames";
+import { Field } from "redux-form";
+
+const zipObject = (props, values) =>
+ props.reduce(
+ (prev, prop, i) => Object.assign(prev, { [prop]: values[i] }),
+ {}
+ );
+
+const changeValue = (checked, item, onChange, currentValue = []) => {
+ if (checked) {
+ if (currentValue.indexOf(checked) === -1) {
+ return onChange([...currentValue, item]);
+ }
+ } else {
+ return onChange(currentValue.filter(items => it === item));
+ }
+ return onChange(currentValue);
+};
+
+const renderChoice = field => {
+ const className = classNames([
+ "form-group",
+ { "has-error": field.meta.touched && field.meta.error }
+ ]);
+ const options = field.schema.items.enum;
+ const optionNames = field.schema.items.enum_titles || options;
+
+ const selectOptions = zipObject(options, optionNames);
+ return (
+
+
+ {Object.entries(selectOptions).map(([value, name]) => (
+
+
+
+ ))}
+
+ {field.meta.touched &&
+ field.meta.error && (
+
{field.meta.error}
+ )}
+ {field.description && (
+
{field.description}
+ )}
+
+ );
+};
+
+const ChoiceMultipleExpandedWidget = props => {
+ return (
+
+ );
+};
+
+export default ChoiceMultipleExpandedWidget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/ChoiceWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/ChoiceWidget.jsx
new file mode 100644
index 000000000..783c94f08
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/ChoiceWidget.jsx
@@ -0,0 +1,78 @@
+import React from "react";
+import PropTypes from "prop-types";
+import classNames from "classnames";
+import { Field } from "redux-form";
+import { zipObject as _zipObject, map as _map } from "lodash";
+
+const renderSelect = field => {
+ const className = classNames([
+ "form-group",
+ { "has-error": field.meta.touched && field.meta.error }
+ ]);
+ const options = field.schema.enum;
+ const optionNames = field.schema.enum_titles || options;
+
+ const selectOptions = _zipObject(options, optionNames);
+ return (
+
+
+
+
+ {field.meta.touched &&
+ field.meta.error && (
+ {field.meta.error}
+ )}
+ {field.description && (
+ {field.description}
+ )}
+
+ );
+};
+
+const ChoiceWidget = props => {
+ return (
+
+ );
+};
+
+ChoiceWidget.propTypes = {
+ schema: PropTypes.object.isRequired,
+ fieldName: PropTypes.string,
+ label: PropTypes.string,
+ theme: PropTypes.object,
+ multiple: PropTypes.bool,
+ required: PropTypes.bool
+};
+
+export default ChoiceWidget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/ColorWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/ColorWidget.jsx
new file mode 100644
index 000000000..8a3b5ea71
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/ColorWidget.jsx
@@ -0,0 +1,17 @@
+import React from "react";
+import PropTypes from "prop-types";
+import BaseInputWidget from "./BaseInputWidget";
+
+const ColorWidget = props => {
+ return ;
+};
+
+BaseInputWidget.propTypes = {
+ schema: PropTypes.object.isRequired,
+ type: PropTypes.string.isRequired,
+ required: PropTypes.bool,
+ fieldName: PropTypes.string,
+ label: PropTypes.string
+};
+
+export default ColorWidget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/CompatibleDateTimeWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/CompatibleDateTimeWidget.jsx
new file mode 100644
index 000000000..8fb85cf33
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/CompatibleDateTimeWidget.jsx
@@ -0,0 +1,192 @@
+import React from "react";
+import classNames from "classnames";
+import { Field } from "redux-form";
+import DateSelector from "./DateSelector";
+
+// produces an array [start..end-1]
+const range = (start, end) =>
+ Array.from({ length: end - start }, (v, k) => k + start);
+
+// produces an array [start..end-1] padded with zeros, (two digits)
+const rangeZeroPad = (start, end) =>
+ Array.from({ length: end - start }, (v, k) => ("0" + (k + start)).slice(-2));
+
+const extractYear = value => {
+ return extractDateTimeToken(value, 0);
+};
+const extractMonth = value => {
+ return extractDateTimeToken(value, 1);
+};
+const extractDay = value => {
+ return extractDateTimeToken(value, 2);
+};
+const extractHour = value => {
+ return extractDateTimeToken(value, 3);
+};
+const extractMinute = value => {
+ return extractDateTimeToken(value, 4);
+};
+const extractSecond = value => {
+ return extractDateTimeToken(value, 5);
+};
+
+const extractDateTimeToken = (value, index) => {
+ if (!value) {
+ return "";
+ }
+ // Remove timezone Z
+ value = value.substring(0, value.length - 1);
+ const tokens = value.split(/[-T:]/);
+ if (tokens.length !== 6) {
+ return "";
+ }
+ return tokens[index];
+};
+
+class CompatibleDateTime extends React.Component {
+ constructor(props, context) {
+ super(props, context);
+ this.state = {
+ year: null,
+ month: null,
+ day: null,
+ hour: null,
+ minute: null,
+ second: null
+ };
+ this.onBlur = this.onBlur.bind(this);
+ }
+
+ // Produces a RFC 3339 full-date from the state
+ buildRfc3339Date() {
+ const year = this.state.year || "";
+ const month = this.state.month || "";
+ const day = this.state.day || "";
+ return year + "-" + month + "-" + day;
+ }
+
+ // Produces a RFC 3339 datetime from the state
+ buildRfc3339DateTime() {
+ const date = this.buildRfc3339Date();
+ const hour = this.state.hour || "";
+ const minute = this.state.minute || "";
+ const second = this.state.second || "";
+ return date + "T" + hour + ":" + minute + ":" + second + "Z";
+ }
+
+ onChangeField(field, e) {
+ const value = e.target.value;
+ let changeset = {};
+ changeset[field] = value;
+ this.setState(changeset, () => {
+ this.props.input.onChange(this.buildRfc3339DateTime());
+ });
+ }
+ onBlur() {
+ this.props.input.onBlur(this.buildRfc3339DateTime());
+ }
+ render() {
+ const field = this.props;
+ const className = classNames([
+ "form-group",
+ { "has-error": field.meta.touched && field.meta.error }
+ ]);
+ return (
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+ {field.meta.touched &&
+ field.meta.error && (
+
{field.meta.error}
+ )}
+ {field.description && (
+
{field.description}
+ )}
+
+ );
+ }
+}
+const CompatibleDateTimeWidget = props => {
+ return (
+
+ );
+};
+
+export default CompatibleDateTimeWidget;
+
+// Only for testing purposes
+export { extractDateTimeToken };
diff --git a/lib/js/liform-react/src/themes/bootstrap3/CompatibleDateWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/CompatibleDateWidget.jsx
new file mode 100644
index 000000000..ef329c0ac
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/CompatibleDateWidget.jsx
@@ -0,0 +1,144 @@
+import React from "react";
+import classNames from "classnames";
+import { Field } from "redux-form";
+import DateSelector from "./DateSelector";
+
+// produces an array [start..end-1]
+const range = (start, end) =>
+ Array.from({ length: end - start }, (v, k) => k + start);
+
+// produces an array [start..end-1] padded with zeros, (two digits)
+const rangeZeroPad = (start, end) =>
+ Array.from({ length: end - start }, (v, k) => ("0" + (k + start)).slice(-2));
+
+const extractYear = value => {
+ return extractDateToken(value, 0);
+};
+const extractMonth = value => {
+ return extractDateToken(value, 1);
+};
+const extractDay = value => {
+ return extractDateToken(value, 2);
+};
+
+const extractDateToken = (value, index) => {
+ if (!value) {
+ return "";
+ }
+ const tokens = value.split(/-/);
+ if (tokens.length !== 3) {
+ return "";
+ }
+ return tokens[index];
+};
+
+class CompatibleDate extends React.Component {
+ constructor(props, context) {
+ super(props, context);
+ this.state = {
+ year: null,
+ month: null,
+ day: null,
+ hour: null,
+ minute: null,
+ second: null
+ };
+ this.onBlur = this.onBlur.bind(this);
+ }
+
+ // Produces a RFC 3339 full-date from the state
+ buildRfc3339Date() {
+ const year = this.state.year || "";
+ const month = this.state.month || "";
+ const day = this.state.day || "";
+ return year + "-" + month + "-" + day;
+ }
+
+ onChangeField(field, e) {
+ const value = e.target.value;
+ let changeset = {};
+ changeset[field] = value;
+ this.setState(changeset, () => {
+ this.props.input.onChange(this.buildRfc3339Date());
+ });
+ }
+
+ onBlur() {
+ this.props.input.onBlur(this.buildRfc3339Date());
+ }
+
+ render() {
+ const field = this.props;
+ const className = classNames([
+ "form-group",
+ { "has-error": field.meta.touched && field.meta.error }
+ ]);
+ return (
+
+
+
+ {field.meta.touched &&
+ field.meta.error && (
+
{field.meta.error}
+ )}
+ {field.description && (
+
{field.description}
+ )}
+
+ );
+ }
+}
+const CompatibleDateWidget = props => {
+ return (
+
+ );
+};
+
+export default CompatibleDateWidget;
+
+// Only for testing purposes
+export { extractDateToken };
diff --git a/lib/js/liform-react/src/themes/bootstrap3/DateSelector.jsx b/lib/js/liform-react/src/themes/bootstrap3/DateSelector.jsx
new file mode 100644
index 000000000..4b5227b6b
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/DateSelector.jsx
@@ -0,0 +1,29 @@
+import React from "react";
+
+const DateSelector = props => {
+ return (
+
+ );
+};
+
+export default DateSelector;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/DateTimeWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/DateTimeWidget.jsx
new file mode 100644
index 000000000..1833f8809
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/DateTimeWidget.jsx
@@ -0,0 +1,8 @@
+import React from "react";
+import BaseInputWidget from "./BaseInputWidget";
+
+const DateTimeWidget = props => {
+ return ;
+};
+
+export default DateTimeWidget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/DateWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/DateWidget.jsx
new file mode 100644
index 000000000..bbc8bc69a
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/DateWidget.jsx
@@ -0,0 +1,8 @@
+import React from "react";
+import BaseInputWidget from "./BaseInputWidget";
+
+const DateWidget = props => {
+ return ;
+};
+
+export default DateWidget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/EmailWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/EmailWidget.jsx
new file mode 100644
index 000000000..5c3aac95a
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/EmailWidget.jsx
@@ -0,0 +1,18 @@
+import React from "react";
+import PropTypes from "prop-types";
+import BaseInputWidget from "./BaseInputWidget";
+
+const EmailWidget = props => {
+ return ;
+};
+
+EmailWidget.propTypes = {
+ schema: PropTypes.object.isRequired,
+ fieldName: PropTypes.string,
+ label: PropTypes.string,
+ theme: PropTypes.object,
+ multiple: PropTypes.bool,
+ required: PropTypes.bool
+};
+
+export default EmailWidget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/FileWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/FileWidget.jsx
new file mode 100644
index 000000000..28a4dd702
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/FileWidget.jsx
@@ -0,0 +1,62 @@
+import React from "react";
+import { Field } from "redux-form";
+import classNames from "classnames";
+
+const processFile = (onChange, e) => {
+ const files = e.target.files;
+ return new Promise(() => {
+ let reader = new FileReader();
+ reader.addEventListener(
+ "load",
+ () => {
+ onChange(reader.result);
+ },
+ false
+ );
+ reader.readAsDataURL(files[0]);
+ });
+};
+
+const File = field => {
+ const className = classNames([
+ "form-group",
+ { "has-error": field.meta.touched && field.meta.error }
+ ]);
+ return (
+
+
+
+ {field.meta.touched &&
+ field.meta.error && (
+ {field.meta.error}
+ )}
+ {field.description && {field.description}}
+
+ );
+};
+
+const FileWidget = props => {
+ return (
+
+ );
+};
+
+export default FileWidget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/MoneyWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/MoneyWidget.jsx
new file mode 100644
index 000000000..722c866f5
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/MoneyWidget.jsx
@@ -0,0 +1,61 @@
+import React from "react";
+import PropTypes from "prop-types";
+import classNames from "classnames";
+import { Field } from "redux-form";
+
+const renderInput = field => {
+ const className = classNames([
+ "form-group",
+ { "has-error": field.meta.touched && field.meta.error }
+ ]);
+ return (
+
+
+
+ €
+
+
+ {field.meta.touched &&
+ field.meta.error && (
+
{field.meta.error}
+ )}
+ {field.description && (
+
{field.description}
+ )}
+
+ );
+};
+
+const MoneyWidget = props => {
+ return (
+
+ );
+};
+
+MoneyWidget.propTypes = {
+ schema: PropTypes.object.isRequired,
+ fieldName: PropTypes.string,
+ label: PropTypes.string,
+ theme: PropTypes.object,
+ multiple: PropTypes.bool,
+ required: PropTypes.bool
+};
+
+export default MoneyWidget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/NumberWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/NumberWidget.jsx
new file mode 100644
index 000000000..00e02be95
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/NumberWidget.jsx
@@ -0,0 +1,8 @@
+import React from "react";
+import BaseInputWidget from "./BaseInputWidget";
+
+const NumberWidget = props => {
+ return ;
+};
+
+export default NumberWidget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/ObjectWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/ObjectWidget.jsx
new file mode 100644
index 000000000..deacdbac4
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/ObjectWidget.jsx
@@ -0,0 +1,27 @@
+import React from "react";
+import PropTypes from "prop-types";
+import renderFields from "../../renderFields";
+
+const Widget = props => {
+ return (
+
+ {props.label && }
+ {renderFields(
+ props.schema,
+ props.theme,
+ props.fieldName && props.fieldName + ".",
+ props.context
+ )}
+
+ );
+};
+
+Widget.propTypes = {
+ schema: PropTypes.object.isRequired,
+ fieldName: PropTypes.string,
+ label: PropTypes.string,
+ theme: PropTypes.object,
+ context: PropTypes.object
+};
+
+export default Widget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/PasswordWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/PasswordWidget.jsx
new file mode 100644
index 000000000..456f2793e
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/PasswordWidget.jsx
@@ -0,0 +1,8 @@
+import React from "react";
+import BaseInputWidget from "./BaseInputWidget";
+
+const PasswordWidget = props => {
+ return ;
+};
+
+export default PasswordWidget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/PercentWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/PercentWidget.jsx
new file mode 100644
index 000000000..89707dd2a
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/PercentWidget.jsx
@@ -0,0 +1,61 @@
+import React from "react";
+import PropTypes from "prop-types";
+import classNames from "classnames";
+import { Field } from "redux-form";
+
+const renderInput = field => {
+ const className = classNames([
+ "form-group",
+ { "has-error": field.meta.touched && field.meta.error }
+ ]);
+ return (
+
+
+
+
+ %
+
+ {field.meta.touched &&
+ field.meta.error && (
+
{field.meta.error}
+ )}
+ {field.description && (
+
{field.description}
+ )}
+
+ );
+};
+
+const Widget = props => {
+ return (
+
+ );
+};
+
+Widget.propTypes = {
+ schema: PropTypes.object.isRequired,
+ fieldName: PropTypes.string,
+ label: PropTypes.string,
+ theme: PropTypes.object,
+ multiple: PropTypes.bool,
+ required: PropTypes.bool
+};
+
+export default Widget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/SearchWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/SearchWidget.jsx
new file mode 100644
index 000000000..8a93e3efa
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/SearchWidget.jsx
@@ -0,0 +1,8 @@
+import React from "react";
+import BaseInputWidget from "./BaseInputWidget";
+
+const SearchWidget = props => {
+ return ;
+};
+
+export default SearchWidget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/StringWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/StringWidget.jsx
new file mode 100644
index 000000000..ad84d401c
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/StringWidget.jsx
@@ -0,0 +1,8 @@
+import React from "react";
+import BaseInputWidget from "./BaseInputWidget";
+
+const StringWidget = props => {
+ return ;
+};
+
+export default StringWidget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/TextareaWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/TextareaWidget.jsx
new file mode 100644
index 000000000..765b35243
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/TextareaWidget.jsx
@@ -0,0 +1,57 @@
+import React from "react";
+import PropTypes from "prop-types";
+import classNames from "classnames";
+import { Field } from "redux-form";
+
+const renderInput = field => {
+ const className = classNames([
+ "form-group",
+ { "has-error": field.meta.touched && field.meta.error }
+ ]);
+ return (
+
+
+
+ {field.meta.touched &&
+ field.meta.error && (
+ {field.meta.error}
+ )}
+ {field.description && (
+ {field.description}
+ )}
+
+ );
+};
+
+const TextareaWidget = props => {
+ return (
+
+ );
+};
+
+TextareaWidget.propTypes = {
+ schema: PropTypes.object.isRequired,
+ fieldName: PropTypes.string,
+ label: PropTypes.string,
+ theme: PropTypes.object,
+ multiple: PropTypes.bool,
+ required: PropTypes.bool
+};
+
+export default TextareaWidget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/TimeWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/TimeWidget.jsx
new file mode 100644
index 000000000..f7bc9a3ad
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/TimeWidget.jsx
@@ -0,0 +1,8 @@
+import React from "react";
+import BaseInputWidget from "./BaseInputWidget";
+
+const TimeWidget = props => {
+ return ;
+};
+
+export default TimeWidget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/UrlWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/UrlWidget.jsx
new file mode 100644
index 000000000..cfaf7da6b
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/UrlWidget.jsx
@@ -0,0 +1,8 @@
+import React from "react";
+import BaseInputWidget from "./BaseInputWidget";
+
+const UrlWidget = props => {
+ return ;
+};
+
+export default UrlWidget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/index.jsx b/lib/js/liform-react/src/themes/bootstrap3/index.jsx
new file mode 100644
index 000000000..252829216
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/index.jsx
@@ -0,0 +1,50 @@
+import StringWidget from "./StringWidget";
+import TextareaWidget from "./TextareaWidget";
+import EmailWidget from "./EmailWidget";
+import NumberWidget from "./NumberWidget";
+import MoneyWidget from "./MoneyWidget";
+import PercentWidget from "./PercentWidget";
+import ArrayWidget from "./ArrayWidget";
+import CheckboxWidget from "./CheckboxWidget";
+import ObjectWidget from "./ObjectWidget";
+import PasswordWidget from "./PasswordWidget";
+import SearchWidget from "./SearchWidget";
+import UrlWidget from "./UrlWidget";
+import ColorWidget from "./ColorWidget";
+import ChoiceWidget from "./ChoiceWidget";
+import ChoiceExpandedWidget from "./ChoiceExpandedWidget";
+import ChoiceMultipleExpandedWidget from "./ChoiceMultipleExpandedWidget";
+import DateWidget from "./DateWidget";
+import TimeWidget from "./TimeWidget";
+import DateTimeWidget from "./DateTimeWidget";
+import CompatibleDateWidget from "./CompatibleDateWidget";
+import CompatibleDateTimeWidget from "./CompatibleDateTimeWidget";
+import FileWidget from "./FileWidget";
+import OneOfChoiceWidget from "./oneOfChoiceWidget";
+
+export default {
+ object: ObjectWidget,
+ string: StringWidget,
+ textarea: TextareaWidget,
+ email: EmailWidget,
+ integer: NumberWidget,
+ number: NumberWidget,
+ money: MoneyWidget,
+ percent: PercentWidget,
+ array: ArrayWidget,
+ boolean: CheckboxWidget,
+ password: PasswordWidget,
+ search: SearchWidget,
+ url: UrlWidget,
+ color: ColorWidget,
+ choice: ChoiceWidget,
+ "choice-expanded": ChoiceExpandedWidget,
+ "choice-multiple-expanded": ChoiceMultipleExpandedWidget,
+ date: DateWidget,
+ datetime: DateTimeWidget,
+ time: TimeWidget,
+ "compatible-date": CompatibleDateWidget,
+ "compatible-datetime": CompatibleDateTimeWidget,
+ file: FileWidget,
+ oneOf: OneOfChoiceWidget
+};
diff --git a/lib/js/liform-react/src/themes/bootstrap3/oneOfChoiceWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/oneOfChoiceWidget.jsx
new file mode 100644
index 000000000..5d435d9b7
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/oneOfChoiceWidget.jsx
@@ -0,0 +1,83 @@
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+import classNames from "classnames";
+import { change } from "redux-form";
+import { connect } from "react-redux";
+import renderField from "../../renderField";
+import { map as _map} from "lodash";
+
+class OneOfChoiceWidget extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ choice: 0
+ };
+ this.renderOption = this.renderOption.bind(this);
+ this.selectItem = this.selectItem.bind(this);
+ }
+
+ render() {
+ const field = this.props;
+ const className = classNames(["form-group"]);
+ const schema = field.schema;
+ const options = schema.oneOf;
+
+ return (
+
+
+
+
{this.renderOption()}
+ {field.description && (
+
{field.description}
+ )}
+
+ );
+ }
+
+ renderOption() {
+ const field = this.props;
+ const schema = field.schema.oneOf[this.state.choice];
+ return renderField(
+ schema,
+ field.fieldName,
+ field.theme,
+ field.prefix,
+ field.context
+ );
+ }
+
+ selectItem(e) {
+ const { schema, context, dispatch } = this.props;
+ for (let property in schema.oneOf[this.state.choice].properties) {
+ dispatch(change(context.formName, property, null));
+ }
+ this.setState({ choice: e.target.value });
+ }
+}
+
+OneOfChoiceWidget.propTypes = {
+ schema: PropTypes.object.isRequired,
+ fieldName: PropTypes.string,
+ label: PropTypes.string,
+ theme: PropTypes.object,
+ multiple: PropTypes.bool,
+ required: PropTypes.bool
+};
+
+export default connect()(OneOfChoiceWidget);
diff --git a/lib/js/liform-react/src/themes/index.jsx b/lib/js/liform-react/src/themes/index.jsx
new file mode 100644
index 000000000..4ec0b6421
--- /dev/null
+++ b/lib/js/liform-react/src/themes/index.jsx
@@ -0,0 +1,42 @@
+import StringWidget from "./StringWidget";
+import TextareaWidget from "./TextareaWidget";
+import EmailWidget from "./EmailWidget";
+import NumberWidget from "./NumberWidget";
+import MoneyWidget from "./MoneyWidget";
+import PercentWidget from "./PercentWidget";
+import ArrayWidget from "./ArrayWidget";
+import CheckboxWidget from "./CheckboxWidget";
+import ObjectWidget from "./ObjectWidget";
+import PasswordWidget from "./PasswordWidget";
+import SearchWidget from "./SearchWidget";
+import UrlWidget from "./UrlWidget";
+import ColorWidget from "./ColorWidget";
+import ChoiceWidget from "./ChoiceWidget";
+import OneOfChoiceWidget from "./oneOfChoiceWidget";
+import DateWidget from "./DateWidget";
+import TimeWidget from "./TimeWidget";
+import DateTimeWidget from "./DateTimeWidget";
+import CompatibleDateWidget from "./CompatibleDateWidget";
+
+export default {
+ object: ObjectWidget,
+ string: StringWidget,
+ textarea: TextareaWidget,
+ email: EmailWidget,
+ integer: NumberWidget,
+ number: NumberWidget,
+ money: MoneyWidget,
+ percent: PercentWidget,
+ array: ArrayWidget,
+ boolean: CheckboxWidget,
+ password: PasswordWidget,
+ search: SearchWidget,
+ url: UrlWidget,
+ color: ColorWidget,
+ choice: ChoiceWidget,
+ date: DateWidget,
+ datetime: DateTimeWidget,
+ time: TimeWidget,
+ OneOfChoiceWidget: OneOfChoiceWidget,
+ "compatible-date": CompatibleDateWidget
+};
diff --git a/lib/js/phrasea-ui/index.ts b/lib/js/phrasea-ui/index.ts
index 721116785..51be8c0c3 100644
--- a/lib/js/phrasea-ui/index.ts
+++ b/lib/js/phrasea-ui/index.ts
@@ -1,10 +1,13 @@
-import NotFoundPage from "./src/components/NotFoundPage";
-import ErrorPage from "./src/components/ErrorPage";
-import ErrorLayout from "./src/components/ErrorLayout";
+import NotFoundPage from "./src/components/Error/NotFoundPage";
+import ErrorPage from "./src/components/Error/ErrorPage";
+import ErrorLayout from "./src/components/Error/ErrorLayout";
+import UserMenu from "./src/components/UserMenu";
+import FullPageLoader from "./src/components/FullPageLoader";
export {
NotFoundPage,
ErrorPage,
ErrorLayout,
+ UserMenu,
+ FullPageLoader,
};
-
diff --git a/lib/js/phrasea-ui/package.json b/lib/js/phrasea-ui/package.json
index 4ffe0d07c..f349351c0 100644
--- a/lib/js/phrasea-ui/package.json
+++ b/lib/js/phrasea-ui/package.json
@@ -4,13 +4,19 @@
"public": true,
"main": "index.ts",
"peerDependencies": {
- "react": "^18.2.0"
- },
- "devDependencies": {
- "@types/react": "^18.2.37",
- "react": "^18.2.0"
+ "react": "^18.2.0",
+ "@mui/material": "^5.15.1",
+ "@mui/icons-material": "^5.15.1",
+ "react-i18next": "^13.5.0"
},
"dependencies": {
"styled-components": "^6.1.1"
+ },
+ "devDependencies": {
+ "@types/react": "^18.2.37",
+ "react": "^18.2.0",
+ "@mui/material": "^5.15.1",
+ "@mui/icons-material": "^5.15.1",
+ "react-i18next": "^13.5.0"
}
}
diff --git a/lib/js/phrasea-ui/src/components/ErrorLayout.tsx b/lib/js/phrasea-ui/src/components/Error/ErrorLayout.tsx
similarity index 100%
rename from lib/js/phrasea-ui/src/components/ErrorLayout.tsx
rename to lib/js/phrasea-ui/src/components/Error/ErrorLayout.tsx
diff --git a/lib/js/phrasea-ui/src/components/ErrorPage.tsx b/lib/js/phrasea-ui/src/components/Error/ErrorPage.tsx
similarity index 100%
rename from lib/js/phrasea-ui/src/components/ErrorPage.tsx
rename to lib/js/phrasea-ui/src/components/Error/ErrorPage.tsx
diff --git a/lib/js/phrasea-ui/src/components/NotFoundPage.tsx b/lib/js/phrasea-ui/src/components/Error/NotFoundPage.tsx
similarity index 100%
rename from lib/js/phrasea-ui/src/components/NotFoundPage.tsx
rename to lib/js/phrasea-ui/src/components/Error/NotFoundPage.tsx
diff --git a/lib/js/phrasea-ui/src/components/FullPageLoader.tsx b/lib/js/phrasea-ui/src/components/FullPageLoader.tsx
new file mode 100644
index 000000000..ba1b1470c
--- /dev/null
+++ b/lib/js/phrasea-ui/src/components/FullPageLoader.tsx
@@ -0,0 +1,17 @@
+import {CircularProgress} from "@mui/material";
+
+type Props = {};
+
+export default function FullPageLoader({}: Props) {
+ return
+
+
+}
diff --git a/lib/js/phrasea-ui/src/components/UserMenu.tsx b/lib/js/phrasea-ui/src/components/UserMenu.tsx
new file mode 100644
index 000000000..6018e9f35
--- /dev/null
+++ b/lib/js/phrasea-ui/src/components/UserMenu.tsx
@@ -0,0 +1,123 @@
+import React, {ReactNode} from 'react';
+import IconButton from '@mui/material/IconButton';
+import Menu from '@mui/material/Menu';
+import Avatar from '@mui/material/Avatar';
+import Tooltip from '@mui/material/Tooltip';
+import MenuItem from '@mui/material/MenuItem';
+import {Divider, ListItemIcon, ListItemText} from '@mui/material';
+import LogoutIcon from '@mui/icons-material/Logout';
+import AccountBoxIcon from '@mui/icons-material/AccountBox';
+import {useTranslation} from 'react-i18next';
+
+type Props = {
+ actions?: (props: {
+ closeMenu: () => void,
+ }) => ReactNode[];
+ accountUrl?: string;
+ onLogout?: () => void;
+ menuHeight: number;
+ username: string;
+};
+
+export default function UserMenu({
+ actions,
+ accountUrl,
+ onLogout,
+ menuHeight,
+ username,
+}: Props) {
+ const {t} = useTranslation();
+ const [anchorElUser, setAnchorElUser] = React.useState(
+ null
+ );
+
+ const handleOpenUserMenu = (event: React.MouseEvent) => {
+ setAnchorElUser(event.currentTarget);
+ };
+
+ const handleCloseUserMenu = () => {
+ setAnchorElUser(null);
+ };
+
+ let menuItems: ReactNode[] = [];
+ if (accountUrl) {
+ menuItems.push()
+ }
+ if (actions) {
+ menuItems = menuItems.concat(actions({
+ closeMenu: handleCloseUserMenu,
+ }));
+ }
+ if (onLogout) {
+ menuItems.push();
+ menuItems.push();
+ }
+
+ return <>
+
+
+
+ {(
+ username[0] || 'U'
+ ).toUpperCase()}
+
+
+
+
+ >
+}
diff --git a/lib/js/phrasea-ui/src/style/error.css b/lib/js/phrasea-ui/src/style/error.css
deleted file mode 100644
index b75ed1d32..000000000
--- a/lib/js/phrasea-ui/src/style/error.css
+++ /dev/null
@@ -1,142 +0,0 @@
-* {
- margin: 0;
- padding: 0;
-}
-
-html {
- height: 100%;
- overflow: hidden;
-}
-
-canvas {
- z-index: 1;
- position: absolute;
- left: 0;
- top: 0;
- width: 100%;
- height: 100%;
-}
-
-.caps {
- z-index: 2;
- position: absolute;
- left: 0;
- top: 0;
- width: 100%;
- height: 100%;
- opacity: 0;
- animation: as 8s linear infinite;
-}
-
-.caps img {
- display: block;
- width: 100%;
- height: 100%;
-}
-
-@keyframes as {
- 0% {
- opacity: 0;
- }
- 10% {
- opacity: .3;
- }
- 20% {
- opacity: .1;
- }
- 30% {
- opacity: .5;
- }
- 40% {
- opacity: 0;
- }
- 50% {
- opacity: .8;
- }
- 55% {
- opacity: 0;
- }
- 55% {
- opacity: 0;
- }
-}
-
-.frame {
- z-index: 3;
- position: absolute;
- left: 0;
- top: 0;
- width: 100%;
- height: 100%;
- background: -moz-radial-gradient(center, ellipse cover, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0) 19%, rgba(0, 0, 0, 0.9) 100%); /* FF3.6+ */
- background: -webkit-gradient(radial, center center, 0px, center center, 100%, color-stop(0%, rgba(0, 0, 0, 0)), color-stop(19%, rgba(0, 0, 0, 0)), color-stop(100%, rgba(0, 0, 0, 0.9))); /* Chrome,Safari4+ */
- background: -webkit-radial-gradient(center, ellipse cover, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0) 19%, rgba(0, 0, 0, 0.9) 100%); /* Chrome10+,Safari5.1+ */
- background: -o-radial-gradient(center, ellipse cover, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0) 19%, rgba(0, 0, 0, 0.9) 100%); /* Opera 12+ */
- background: -ms-radial-gradient(center, ellipse cover, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0) 19%, rgba(0, 0, 0, 0.9) 100%); /* IE10+ */
- background: radial-gradient(ellipse at center, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0) 19%, rgba(0, 0, 0, 0.9) 100%); /* W3C */
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#00000000', endColorstr = '#e6000000', GradientType = 1); /* IE6-9 fallback on horizontal gradient */
-
-}
-
-.error-frame div {
- position: absolute;
- left: 0;
- top: -20%;
- width: 100%;
- height: 20%;
- background-color: rgba(0, 0, 0, .12);
- box-shadow: 0 0 10px rgba(0, 0, 0, .3);
- animation: asd 12s linear infinite;
-}
-
-.frame div:nth-child(1) {
- animation-delay: 0;
-}
-
-.frame div:nth-child(2) {
- animation-delay: 4s;
-}
-
-.frame div:nth-child(3) {
- animation-delay: 8s;
-}
-
-@keyframes asd {
- 0% {
- top: -20%;
- }
- 100% {
- top: 100%;
- }
-}
-
-h1 {
- z-index: 3;
- position: absolute;
- font: bold 200px/200px Arial, sans-serif;
- left: 50%;
- top: 50%;
- margin-top: -100px;
- width: 100%;
- margin-left: -50%;
- height: 200px;
- text-align: center;
- color: transparent;
- text-shadow: 0 0 30px rgba(0, 0, 0, .5);
- animation: asdd 2s linear infinite;
-}
-
-@keyframes asdd {
- 0% {
- text-shadow: 0 0 30px rgba(0, 0, 0, .5);
- }
- 33% {
- text-shadow: 0 0 10px rgba(0, 0, 0, .4);
- }
- 66% {
- text-shadow: 0 0 20px rgba(0, 0, 0, .2);
- }
- 100% {
- text-shadow: 0 0 40px rgba(0, 0, 0, .8);
- }
-}
diff --git a/lib/js/react-auth/index.ts b/lib/js/react-auth/index.ts
index 67fcd72dc..9b667dc55 100644
--- a/lib/js/react-auth/index.ts
+++ b/lib/js/react-auth/index.ts
@@ -6,6 +6,9 @@ import {useKeycloakUrls} from "./src/hooks/useKeycloakUrls";
import AuthorizationCodePage from "./src/components/AuthorizationCodePage";
import MatomoUser from "./src/components/MatomoUser";
import {useForceLogin} from "./src/hooks/useForceLogin";
+import SessionAboutToExpireModal from "./src/components/SessionAboutToExpireModal";
+import SessionExpireContainer from "./src/components/SessionExpireContainer";
+import {useAuthorizationCode} from "./src/hooks/useAuthorizationCode";
export {
AuthenticationContext,
@@ -17,6 +20,9 @@ export {
useKeycloakUrls,
MatomoUser,
useForceLogin,
+ SessionAboutToExpireModal,
+ SessionExpireContainer,
+ useAuthorizationCode,
};
export type {
TAuthContext,
diff --git a/lib/js/react-auth/package.json b/lib/js/react-auth/package.json
index 03a35e97a..df418b7db 100644
--- a/lib/js/react-auth/package.json
+++ b/lib/js/react-auth/package.json
@@ -11,7 +11,7 @@
"react-router-dom": "^6.18.0",
"react-i18next": "^13.5.0",
"react-toastify": "^9.1.3",
- "@mui/material": "^5.14.19"
+ "@mui/material": "^5.15.1"
},
"dependencies": {
"jwt-decode": "^4.0.0",
@@ -32,7 +32,8 @@
"typescript": "^5.3.2",
"react-i18next": "^13.5.0",
"react-router-dom": "^6.18.0",
- "@mui/material": "^5.14.19",
+ "@mui/material": "^5.15.1",
+ "@mui/lab": "^5.0.0-alpha.157",
"@jonkoops/matomo-tracker-react": "^0.7.0"
}
}
diff --git a/lib/js/react-auth/src/components/AuthenticationProvider.tsx b/lib/js/react-auth/src/components/AuthenticationProvider.tsx
index 9186f2bc5..5720db774 100644
--- a/lib/js/react-auth/src/components/AuthenticationProvider.tsx
+++ b/lib/js/react-auth/src/components/AuthenticationProvider.tsx
@@ -1,14 +1,15 @@
import React, {PropsWithChildren, useCallback} from 'react';
import {
- OAuthClient,
- isValidSession,
+ AuthEventHandler,
AuthTokens,
+ isValidSession,
+ LogoutEvent,
logoutEventType,
- AuthEventHandler,
- LogoutEvent
+ OAuthClient,
+ RefreshTokenEvent, refreshTokenEventType
} from "@alchemy/auth";
import {getSessionStorage} from "@alchemy/storage";
-import AuthenticationContext, {LogoutFunction, SetTokens} from "../context/AuthenticationContext";
+import AuthenticationContext, {LogoutFunction, RefreshTokenFunction, SetTokens} from "../context/AuthenticationContext";
type Props = PropsWithChildren<{
onNewTokens?: (tokens: AuthTokens) => void;
@@ -27,16 +28,24 @@ export default function AuthenticationProvider({
const [tokens, setTokens] = React.useState(oauthClient.getTokens());
React.useEffect(() => {
- const listener: AuthEventHandler = async (event) => {
+ const logoutListener: AuthEventHandler = async (event) => {
if (!event.preventDefault) {
setTokens(undefined);
}
};
- oauthClient.registerListener(logoutEventType, listener);
+ const refreshTokenListener: AuthEventHandler = async (event) => {
+ if (!event.preventDefault) {
+ setTokens(event.tokens);
+ }
+ };
+
+ oauthClient.registerListener(logoutEventType, logoutListener);
+ oauthClient.registerListener(refreshTokenEventType, refreshTokenListener);
return () => {
- oauthClient.unregisterListener(logoutEventType, listener);
+ oauthClient.unregisterListener(logoutEventType, logoutListener);
+ oauthClient.unregisterListener(refreshTokenEventType, refreshTokenListener);
}
}, [oauthClient]);
@@ -45,6 +54,10 @@ export default function AuthenticationProvider({
onNewTokens && onNewTokens(tokens);
}, [setTokens]);
+ const refreshToken = React.useCallback(async () => {
+ return await oauthClient.getTokenFromRefreshToken();
+ }, [oauthClient]);
+
const setRedirectPath = React.useCallback((path: string | undefined) => {
redirectPath.current = path;
@@ -59,7 +72,10 @@ export default function AuthenticationProvider({
setRedirectPath(undefined);
}, [setRedirectPath]);
- const logout = useCallback(async (redirectPathAfterLogin?: string, quiet = false) => {
+ const logout = useCallback(async ({
+ redirectPathAfterLogin,
+ ...options
+ } = {}) => {
const handler = () => {
if (redirectPathAfterLogin) {
setRedirectPath(redirectPathAfterLogin);
@@ -70,8 +86,7 @@ export default function AuthenticationProvider({
}
}
- const event = await oauthClient.logout({quiet});
- console.log('event', event);
+ const event = await oauthClient.logout(options);
if (event?.preventDefault) {
handler();
@@ -94,6 +109,7 @@ export default function AuthenticationProvider({
redirectPath,
clearRedirectPath,
isAuthenticated,
+ refreshToken: tokens ? refreshToken : undefined,
}}
>
{children}
diff --git a/lib/js/react-auth/src/components/AuthorizationCodePage.tsx b/lib/js/react-auth/src/components/AuthorizationCodePage.tsx
index 76670fd27..693591fbe 100644
--- a/lib/js/react-auth/src/components/AuthorizationCodePage.tsx
+++ b/lib/js/react-auth/src/components/AuthorizationCodePage.tsx
@@ -1,83 +1,18 @@
-import useEffectOnce from "@alchemy/react-hooks/src/useEffectOnce";
-import {OAuthClient} from "@alchemy/auth";
import {useNavigate} from "react-router-dom";
-import {useAuth} from "../hooks/useAuth";
import {toast} from "react-toastify";
import React from "react";
+import {useAuthorizationCode, UseAuthorizationCodeProps} from "../hooks/useAuthorizationCode";
-type Props = {
- oauthClient: OAuthClient,
- successUri?: string,
- successHandler?: () => void,
- errorHandler?: (e: any) => void,
-};
+type Props = Omit;
-export default function AuthorizationCodePage({
- oauthClient,
- successUri,
- successHandler,
- errorHandler,
-}: Props) {
+export default function AuthorizationCodePage(props: Props) {
const navigate = useNavigate();
- const urlParams = new URLSearchParams(window.location.search);
- const {setTokens} = useAuth();
- const [error, setError] = React.useState();
-
- useEffectOnce(() => {
- const code = urlParams.get('code');
- if (!code) {
- setError(new Error(`Missing authorization code.`));
-
- return;
- }
-
- const state = urlParams.get('state');
-
- oauthClient.getTokenFromAuthCode(
- code,
- window.location.href.split('?')[0]
- )
- .then((tokens) => {
- setTokens(tokens);
- if (successHandler) {
- successHandler();
-
- return;
- }
-
- if (state) {
- try {
- const dState = JSON.parse(atob(state)) as {
- r?: string;
- };
- // eslint-disable-next-line no-prototype-builtins
- if (
- typeof dState === 'object' &&
- // eslint-disable-next-line
- dState.hasOwnProperty('r') &&
- typeof dState.r === 'string'
- ) {
- navigate(dState.r);
-
- return;
- }
- } catch (e: any) {
- // Ignore
- }
- }
-
- navigate(successUri ?? '/', {replace: true});
- })
- .catch ((e) => {
- if (errorHandler) {
- errorHandler(e);
-
- return ;
- }
-
- setError(e);
- });
- }, []);
+ const {
+ error
+ } = useAuthorizationCode({
+ navigate: (path, options) => navigate(path, options),
+ ...props,
+ });
React.useEffect(() => {
if (error) {
diff --git a/lib/js/react-auth/src/components/SessionAboutToExpire.tsx b/lib/js/react-auth/src/components/SessionAboutToExpire.tsx
deleted file mode 100644
index f2c0e0723..000000000
--- a/lib/js/react-auth/src/components/SessionAboutToExpire.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import Button from '@mui/material/Button';
-import Dialog from '@mui/material/Dialog';
-import DialogActions from '@mui/material/DialogActions';
-import DialogContent from '@mui/material/DialogContent';
-import DialogContentText from '@mui/material/DialogContentText';
-import DialogTitle from '@mui/material/DialogTitle';
-import {useTranslation} from 'react-i18next';
-import React from "react";
-import {useModals, StackedModalProps} from '@alchemy/navigation'
-import AuthenticationContext from "../context/AuthenticationContext";
-
-type Props = {
- expiresIn: number;
- cancelLogout: () => void;
-} & StackedModalProps;
-
-export default function SessionAboutToExpire({
- open,
- expiresIn,
- cancelLogout,
-}: Props) {
- const {t} = useTranslation();
- const {logout, addLogoutListener, removeLogoutListener} = React.useContext(AuthenticationContext);
- const {closeModal} = useModals();
- const closeTimeout = React.useRef | null>(null);
- const [refreshing, setRefreshing] = React.useState(false);
-
- const clear = () => {
- if (closeTimeout.current) {
- clearTimeout(closeTimeout.current);
- closeTimeout.current = null;
- }
- }
-
- const handleClose = () => {
- clear();
- closeModal();
- };
-
- React.useEffect(() => {
- clear();
- closeTimeout.current = setTimeout(handleClose, expiresIn);
- }, []);
-
- React.useEffect(() => {
- const listener = () => {
- handleClose();
- };
- addLogoutListener(listener);
-
- return () => {
- removeLogoutListener(listener);
- }
- }, []);
-
- const stay = async () => {
- cancelLogout();
- clear();
- setRefreshing(true);
- try {
- handleClose();
- } catch (e: any) {
- if (e.isAxiosError && e.response?.status === 401) {
- logout();
- }
- } finally {
- setRefreshing(false);
- }
- }
-
- return
-}
diff --git a/lib/js/react-auth/src/components/SessionAboutToExpireModal.tsx b/lib/js/react-auth/src/components/SessionAboutToExpireModal.tsx
new file mode 100644
index 000000000..1d9b6b834
--- /dev/null
+++ b/lib/js/react-auth/src/components/SessionAboutToExpireModal.tsx
@@ -0,0 +1,88 @@
+import {Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle} from '@mui/material';
+import LoadingButton from '@mui/lab/LoadingButton';
+import {useTranslation} from 'react-i18next';
+import React from "react";
+import {useAuth} from "../hooks/useAuth";
+
+type Props = {
+ onClose: () => void;
+};
+
+export default function SessionAboutToExpireModal({
+ onClose,
+}: Props) {
+ const {t} = useTranslation();
+ const {logout, refreshToken} = useAuth();
+ const [refreshing, setRefreshing] = React.useState(false);
+
+ const handleClose = () => {
+ onClose();
+ }
+
+ const stay = async () => {
+ if (!refreshToken) {
+ return;
+ }
+ setRefreshing(true);
+ try {
+ await refreshToken();
+ handleClose();
+ } catch (e: any) {
+ if (e.isAxiosError && e.response?.status === 401) {
+ logout();
+ }
+ } finally {
+ setRefreshing(false);
+ }
+ }
+
+ const expired = !Boolean(refreshToken);
+
+ return
+}
diff --git a/lib/js/react-auth/src/components/SessionExpireContainer.tsx b/lib/js/react-auth/src/components/SessionExpireContainer.tsx
new file mode 100644
index 000000000..7f2f6af35
--- /dev/null
+++ b/lib/js/react-auth/src/components/SessionExpireContainer.tsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import {useTimeout} from "@alchemy/react-hooks/src/useTimeout";
+import SessionAboutToExpireModal from "./SessionAboutToExpireModal";
+import {useAuth} from "../hooks/useAuth";
+
+type Props = {};
+
+export default function SessionExpireContainer({}: Props) {
+ const {tokens} = useAuth();
+ const [displayExpire, setDisplayExpire] = React.useState(false);
+
+ const displayExpireModal = React.useCallback(() => {
+ setDisplayExpire(true);
+ }, [tokens]);
+
+ const onClose = React.useCallback(() => {
+ setDisplayExpire(false);
+ }, []);
+
+ let delay: number | undefined = undefined;
+ if (tokens?.refreshExpiresAt) {
+ const beforeEnd = 60000;
+ const end = tokens.refreshExpiresAt * 1000 - new Date().getTime();
+ delay = Math.max(end - beforeEnd, 5000)
+ }
+
+ useTimeout(displayExpireModal, delay);
+
+ return <>
+ {displayExpire && }
+ >
+}
diff --git a/lib/js/react-auth/src/context/AuthenticationContext.ts b/lib/js/react-auth/src/context/AuthenticationContext.ts
index dc17aeb51..900591a76 100644
--- a/lib/js/react-auth/src/context/AuthenticationContext.ts
+++ b/lib/js/react-auth/src/context/AuthenticationContext.ts
@@ -1,13 +1,19 @@
import {createContext, MutableRefObject} from 'react';
-import {AuthTokens} from "@alchemy/auth";
+import {AuthTokens, LogoutOptions} from "@alchemy/auth";
export type SetTokens = (tokens: AuthTokens) => void;
+export type RefreshTokenFunction = () => Promise;
-export type LogoutFunction = (redirectPathAfterLogin?: string, quiet?: boolean) => void;
+type ExtendedLogoutOptions = {
+ redirectPathAfterLogin?: string;
+} & LogoutOptions;
+
+export type LogoutFunction = (options?: ExtendedLogoutOptions) => void;
export type TAuthContext = {
tokens?: AuthTokens | undefined;
logout: LogoutFunction;
+ refreshToken?: RefreshTokenFunction;
setTokens: SetTokens;
setRedirectPath?: ((url: string) => void) | undefined;
clearRedirectPath: () => void;
diff --git a/lib/js/react-auth/src/hooks/useAuthorizationCode.ts b/lib/js/react-auth/src/hooks/useAuthorizationCode.ts
new file mode 100644
index 000000000..b6c71fa0c
--- /dev/null
+++ b/lib/js/react-auth/src/hooks/useAuthorizationCode.ts
@@ -0,0 +1,95 @@
+import useEffectOnce from '@alchemy/react-hooks/src/useEffectOnce'
+import React from "react";
+import {useAuth} from "./useAuth";
+import {OAuthClient} from "@alchemy/auth";
+
+type Props = {
+ navigate: (path: string, options?: {
+ replace?: boolean;
+ }) => void;
+ oauthClient: OAuthClient,
+ successUri?: string,
+ successHandler?: () => void,
+ errorHandler?: (e: any) => void,
+};
+
+export type {Props as UseAuthorizationCodeProps};
+
+export function useAuthorizationCode({
+ oauthClient,
+ successHandler,
+ successUri,
+ errorHandler,
+ navigate,
+ allowNoCode,
+}: {
+ allowNoCode?: boolean;
+} & Props) {
+ const {setTokens} = useAuth();
+ const [error, setError] = React.useState();
+ const urlParams = new URLSearchParams(window.location.search);
+ const code = urlParams.get('code');
+
+ useEffectOnce(() => {
+ const code = urlParams.get('code');
+ if (!code) {
+ if (!allowNoCode) {
+ setError(new Error(`Missing authorization code.`));
+ }
+
+ return;
+ }
+
+ const state = urlParams.get('state');
+
+ oauthClient.getTokenFromAuthCode(
+ code,
+ window.location.href.split('?')[0]
+ )
+ .then((tokens) => {
+ setTokens(tokens);
+ if (successHandler) {
+ successHandler();
+
+ return;
+ }
+
+ if (state) {
+ try {
+ const dState = JSON.parse(atob(state)) as {
+ r?: string;
+ };
+ // eslint-disable-next-line no-prototype-builtins
+ if (
+ typeof dState === 'object' &&
+ // eslint-disable-next-line
+ dState.hasOwnProperty('r') &&
+ typeof dState.r === 'string'
+ ) {
+ navigate(dState.r);
+
+ return;
+ }
+ } catch (e: any) {
+ // Ignore
+ }
+ }
+
+ navigate(successUri ?? '/', {replace: true});
+ })
+ .catch ((e) => {
+ if (errorHandler) {
+ errorHandler(e);
+
+ return ;
+ }
+
+ setError(e);
+ });
+ }, []);
+
+ return {
+ error,
+ hasCode: Boolean(code),
+ }
+}
diff --git a/lib/js/react-auth/src/hooks/useSessionExpire.ts b/lib/js/react-auth/src/hooks/useSessionExpire.ts
deleted file mode 100644
index a05a3456b..000000000
--- a/lib/js/react-auth/src/hooks/useSessionExpire.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import React from "react";
-import AuthenticationContext from "../context/AuthenticationContext";
-import {getFullPath, useModals} from '@alchemy/navigation'
-import SessionAboutToExpire from "../components/SessionAboutToExpire";
-
-type Options = {
- noticeBeforeEnd?: number;
- idleTimeout?: number;
-}
-
-export function useSessionExpire({
- noticeBeforeEnd = 60000,
- idleTimeout = 60000,
-}: Options) {
- const {openModal} = useModals();
- const {tokens, logout} = React.useContext(AuthenticationContext);
- const sessionTimeout = React.useRef | null>(null);
- const promptTimeout = React.useRef | null>(null);
- const idle = React.useRef(false);
- const sessionExpiredToastId = 'sess_exp';
-
- const sessionExpiredToast = () => {
- toast.warning(t('auth:session_expired', 'Your session has expired'), {
- toastId: sessionExpiredToastId,
- });
- };
-
- const cancelExpiration = () => {
- if (sessionTimeout.current) {
- clearTimeout(sessionTimeout.current);
- sessionTimeout.current = null;
- }
- if (promptTimeout.current) {
- clearTimeout(promptTimeout.current);
- promptTimeout.current = null;
- }
- }
-
- React.useEffect(() => {
- const unregisterIdle = listenIdle((isIdle) => {
- idle.current = isIdle;
- }, idleTimeout);
-
- return () => {
- unregisterIdle();
- }
- }, [idleTimeout]);
-
- React.useEffect(() => {
- if (tokens.refreshExpiresIn) {
- sessionTimeout.current = setTimeout(() => {
- sessionExpiredToast();
- logout(getFullPath());
- }, tokens.refreshExpiresIn);
-
- promptTimeout.current = setTimeout(() => {
- if (idle.current) {
- openModal(SessionAboutToExpire, {
- expiresIn: noticeBeforeEnd,
- cancelLogout: () => {
- cancelExpiration();
- }
- });
- } else {
- cancelExpiration();
- await refreshToken();
- refreshUser(getAuthUser()!);
- }
- }, tokens.refreshExpiresIn - noticeBeforeEnd);
- }
- }, [tokens]);
-
- React.useEffect(() => {
- const listener = () => {
- cancelExpiration();
- };
- addLogoutListener(listener);
-
- return () => {
- removeLogoutListener(listener);
- }
- }, [])
-}
-
-
-function listenIdle(onChange: (isIdle: boolean) => void, idleTimeout: number) {
- let time: ReturnType;
-
- function resetTimer() {
- onChange(false);
- clearTimeout(time);
- time = setTimeout(() => {
- onChange(true);
- }, idleTimeout);
- }
-
- window.addEventListener('load', resetTimer);
- document.addEventListener('mousemove', resetTimer);
- document.addEventListener('keydown', resetTimer);
- document.addEventListener('touchstart', resetTimer);
-
- return () => {
- clearTimeout(time);
- window.removeEventListener('load', resetTimer);
- document.removeEventListener('mousemove', resetTimer);
- document.removeEventListener('keydown', resetTimer);
- document.removeEventListener('touchstart', resetTimer);
- };
-}
diff --git a/lib/js/react-hooks/package.json b/lib/js/react-hooks/package.json
index 99c290993..b6717127e 100644
--- a/lib/js/react-hooks/package.json
+++ b/lib/js/react-hooks/package.json
@@ -6,7 +6,8 @@
"src/useDebounceLoader.ts",
"src/useDebounceScroll.ts",
"src/useEffectOnce.ts",
- "src/useWindowSize.ts"
+ "src/useWindowSize.ts",
+ "src/useTimeout.ts"
],
"dependencies": {},
"peerDependencies": {
diff --git a/lib/js/react-hooks/src/useTimeout.ts b/lib/js/react-hooks/src/useTimeout.ts
new file mode 100644
index 000000000..7a8b7994b
--- /dev/null
+++ b/lib/js/react-hooks/src/useTimeout.ts
@@ -0,0 +1,23 @@
+import React from "react";
+
+export function useTimeout(handler: () => void | undefined, delay: number | undefined) {
+ const timeoutRef = React.useRef>();
+
+ React.useEffect(() => {
+ if (timeoutRef.current) {
+ clearTimeout(timeoutRef.current);
+ }
+
+ if (undefined !== delay && handler) {
+ timeoutRef.current = setTimeout(handler, delay);
+ }
+
+ return () => {
+ if (timeoutRef.current) {
+ clearTimeout(timeoutRef.current);
+ }
+ }
+ }, [delay, handler]);
+
+ return timeoutRef;
+}
diff --git a/lib/js/theme-editor/package.json b/lib/js/theme-editor/package.json
index 203310f94..049f02eb2 100644
--- a/lib/js/theme-editor/package.json
+++ b/lib/js/theme-editor/package.json
@@ -9,7 +9,7 @@
"peerDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
- "@mui/material": "^5.15.0",
+ "@mui/material": "^5.15.1",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0"
},
@@ -21,7 +21,7 @@
"@types/react-dom": "^18.2.15",
"react": "^18.2.0",
"react-dom": "^18.2.0",
- "@mui/material": "^5.15.0",
+ "@mui/material": "^5.15.1",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0"
}
diff --git a/lib/js/visual-workflow/src/stories/Updates.stories.tsx b/lib/js/visual-workflow/src/stories/Updates.stories.tsx
index 7fde91857..553c1b85e 100644
--- a/lib/js/visual-workflow/src/stories/Updates.stories.tsx
+++ b/lib/js/visual-workflow/src/stories/Updates.stories.tsx
@@ -30,7 +30,6 @@ type Story = StoryObj;
const rerun = async (jobId: string): Promise => {
return new Promise((resolve) => {
- console.log(`Rerunning job ${jobId}`);
setTimeout(() => {
resolve();
}, 200);
diff --git a/lib/js/visual-workflow/src/stories/VisualWorkflow.stories.tsx b/lib/js/visual-workflow/src/stories/VisualWorkflow.stories.tsx
index 6929ef1c3..83c01d933 100644
--- a/lib/js/visual-workflow/src/stories/VisualWorkflow.stories.tsx
+++ b/lib/js/visual-workflow/src/stories/VisualWorkflow.stories.tsx
@@ -30,7 +30,6 @@ type Story = StoryObj;
const rerun = async (jobId: string): Promise => {
return new Promise((resolve) => {
- console.log(`Rerunning job ${jobId}`);
setTimeout(() => {
resolve();
}, 1000);
diff --git a/lib/js/visual-workflow/src/stories/WorkflowHeader.stories.tsx b/lib/js/visual-workflow/src/stories/WorkflowHeader.stories.tsx
index 0f79904f5..c1fb6489c 100644
--- a/lib/js/visual-workflow/src/stories/WorkflowHeader.stories.tsx
+++ b/lib/js/visual-workflow/src/stories/WorkflowHeader.stories.tsx
@@ -32,7 +32,6 @@ type Story = StoryObj;
const refresh = async (): Promise => {
return new Promise((resolve) => {
- console.log(`Refreshing workflow`);
setTimeout(() => {
resolve();
}, 1000);
diff --git a/lib/php/admin-bundle/Resources/config/services.yaml b/lib/php/admin-bundle/Resources/config/services.yaml
index c41338f3c..7d5738b3e 100644
--- a/lib/php/admin-bundle/Resources/config/services.yaml
+++ b/lib/php/admin-bundle/Resources/config/services.yaml
@@ -1,7 +1,7 @@
parameters:
- env(DASHBOARD_URL): ~
+ env(DASHBOARD_CLIENT_URL): ~
env(DISPLAY_SERVICES_MENU): false
- alchemy_admin.dashboard_menu_url: '%env(default::DASHBOARD_URL)%/menu.html'
+ alchemy_admin.dashboard_menu_url: '%env(default::DASHBOARD_CLIENT_URL)%/menu.html'
alchemy_admin.services_menu_enabled: '%env(bool:DISPLAY_SERVICES_MENU)%'
services:
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 94f04918f..5372c3b01 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -17,15 +17,30 @@ importers:
dashboard/client:
dependencies:
+ '@alchemy/api':
+ specifier: workspace:*
+ version: link:../../lib/js/api
+ '@alchemy/auth':
+ specifier: workspace:*
+ version: link:../../lib/js/auth
+ '@alchemy/core':
+ specifier: workspace:*
+ version: link:../../lib/js/core
+ '@alchemy/phrasea-ui':
+ specifier: workspace:*
+ version: link:../../lib/js/phrasea-ui
+ '@alchemy/react-auth':
+ specifier: workspace:*
+ version: link:../../lib/js/react-auth
'@alchemy/theme-editor':
specifier: workspace:*
version: link:../../lib/js/theme-editor
'@mui/icons-material':
specifier: ^5.15.0
- version: 5.15.0(@mui/material@5.15.0)(@types/react@18.2.39)(react@18.2.0)
+ version: 5.15.0(@mui/material@5.15.1)(@types/react@18.2.39)(react@18.2.0)
'@mui/material':
- specifier: ^5.15.0
- version: 5.15.0(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0)
+ specifier: ^5.15.1
+ version: 5.15.1(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0)
react:
specifier: ^18.2.0
version: 18.2.0
@@ -35,6 +50,9 @@ importers:
react-google-font-loader:
specifier: ^1.1.0
version: 1.1.0(react-dom@18.2.0)(react@18.2.0)
+ react-i18next:
+ specifier: ^13.5.0
+ version: 13.5.0(i18next@23.7.7)(react-dom@18.2.0)(react@18.2.0)
vite-plugin-svgr:
specifier: ^4.2.0
version: 4.2.0(typescript@5.3.2)(vite@5.0.7)
@@ -84,6 +102,9 @@ importers:
'@alchemy/navigation':
specifier: workspace:*
version: link:../../lib/js/navigation
+ '@alchemy/phrasea-ui':
+ specifier: workspace:*
+ version: link:../../lib/js/phrasea-ui
'@alchemy/react-auth':
specifier: workspace:*
version: link:../../lib/js/react-auth
@@ -116,13 +137,13 @@ importers:
version: 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.39)(react@18.2.0)
'@mui/icons-material':
specifier: ^5.15.0
- version: 5.15.0(@mui/material@5.15.0)(@types/react@18.2.39)(react@18.2.0)
+ version: 5.15.0(@mui/material@5.15.1)(@types/react@18.2.39)(react@18.2.0)
'@mui/lab':
- specifier: ^5.0.0-alpha.156
- version: 5.0.0-alpha.156(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.15.0)(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0)
+ specifier: ^5.0.0-alpha.157
+ version: 5.0.0-alpha.157(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.15.1)(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0)
'@mui/material':
- specifier: ^5.15.0
- version: 5.15.0(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0)
+ specifier: ^5.15.1
+ version: 5.15.1(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0)
'@toast-ui/react-image-editor':
specifier: ^3.15.2
version: 3.15.2(react@18.2.0)
@@ -149,7 +170,7 @@ importers:
version: 2.4.5(react@18.2.0)
formik-material-ui:
specifier: ^4.0.0-alpha.2
- version: 4.0.0-alpha.2(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.15.0)(formik@2.4.5)(react@18.2.0)(tiny-warning@1.0.3)
+ version: 4.0.0-alpha.2(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.15.1)(formik@2.4.5)(react@18.2.0)(tiny-warning@1.0.3)
i18next:
specifier: ^21.8.0
version: 21.10.0
@@ -600,6 +621,40 @@ importers:
specifier: ^18.2.37
version: 18.2.39
+ lib/js/liform-react:
+ dependencies:
+ ajv:
+ specifier: ^8.12.0
+ version: 8.12.0
+ classnames:
+ specifier: ^2.2.5
+ version: 2.3.2
+ deepmerge:
+ specifier: ^2.0.1
+ version: 2.2.1
+ lodash:
+ specifier: ^4.17.21
+ version: 4.17.21
+ prop-types:
+ specifier: ^15.5.10
+ version: 15.8.1
+ react-redux:
+ specifier: ^9.0.4
+ version: 9.0.4(@types/react@18.2.38)(react@18.2.0)(redux@4.2.1)
+ redux:
+ specifier: ^4.2.1
+ version: 4.2.1
+ redux-form:
+ specifier: ^8.3.10
+ version: 8.3.10(react-redux@9.0.4)(react@18.2.0)(redux@4.2.1)
+ devDependencies:
+ react:
+ specifier: ^18.2.0
+ version: 18.2.0
+ react-dom:
+ specifier: ^18.2.0
+ version: 18.2.0(react@18.2.0)
+
lib/js/navigation:
dependencies:
'@alchemy/core':
@@ -637,12 +692,21 @@ importers:
specifier: ^6.1.1
version: 6.1.1(react-dom@18.2.0)(react@18.2.0)
devDependencies:
+ '@mui/icons-material':
+ specifier: ^5.15.1
+ version: 5.15.1(@mui/material@5.15.1)(@types/react@18.2.39)(react@18.2.0)
+ '@mui/material':
+ specifier: ^5.15.1
+ version: 5.15.1(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0)
'@types/react':
specifier: ^18.2.37
version: 18.2.39
react:
specifier: ^18.2.0
version: 18.2.0
+ react-i18next:
+ specifier: ^13.5.0
+ version: 13.5.0(i18next@23.7.7)(react-dom@18.2.0)(react@18.2.0)
lib/js/react-auth:
dependencies:
@@ -668,9 +732,12 @@ importers:
'@jonkoops/matomo-tracker-react':
specifier: ^0.7.0
version: 0.7.0(react@18.2.0)
+ '@mui/lab':
+ specifier: ^5.0.0-alpha.157
+ version: 5.0.0-alpha.157(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.15.1)(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0)
'@mui/material':
- specifier: ^5.14.19
- version: 5.14.19(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0)
+ specifier: ^5.15.1
+ version: 5.15.1(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0)
'@types/i18next':
specifier: ^13.0.0
version: 13.0.0
@@ -884,8 +951,8 @@ importers:
specifier: ^11.11.0
version: 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.39)(react@18.2.0)
'@mui/material':
- specifier: ^5.15.0
- version: 5.15.0(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0)
+ specifier: ^5.15.1
+ version: 5.15.1(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0)
'@types/react':
specifier: ^18.2.38
version: 18.2.39
@@ -971,9 +1038,6 @@ importers:
uploader/client:
dependencies:
- '@alchemy-fr/liform-react':
- specifier: ^0.9.3
- version: 0.9.3(react-redux@5.1.2)(redux-form@7.4.3)(redux@4.2.1)
'@alchemy/api':
specifier: workspace:*
version: link:../../lib/js/api
@@ -983,6 +1047,9 @@ importers:
'@alchemy/core':
specifier: workspace:*
version: link:../../lib/js/core
+ '@alchemy/liform-react':
+ specifier: workspace:*
+ version: link:../../lib/js/liform-react
'@alchemy/navigation':
specifier: workspace:*
version: link:../../lib/js/navigation
@@ -995,6 +1062,9 @@ importers:
'@alchemy/react-ps':
specifier: workspace:*
version: link:../../lib/js/react-ps
+ '@reduxjs/toolkit':
+ specifier: ^2.0.1
+ version: 2.0.1(react-redux@9.0.4)(react@18.2.0)
axios:
specifier: ^1.6.2
version: 1.6.2
@@ -1035,17 +1105,20 @@ importers:
specifier: ^3.1.14
version: 3.1.14(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0)
react-redux:
- specifier: ^5.1
- version: 5.1.2(react@18.2.0)(redux@4.2.1)
+ specifier: ^9.0.4
+ version: 9.0.4(@types/react@18.2.38)(react@18.2.0)(redux@4.2.1)
react-toastify:
specifier: ^9.1.3
version: 9.1.3(react-dom@18.2.0)(react@18.2.0)
redux:
- specifier: ^4.0.1
+ specifier: ^4.2.1
version: 4.2.1
redux-form:
- specifier: ^7.4.2
- version: 7.4.3(react-redux@5.1.2)(react@18.2.0)(redux@4.2.1)
+ specifier: ^8.3.10
+ version: 8.3.10(react-redux@9.0.4)(react@18.2.0)(redux@4.2.1)
+ url:
+ specifier: ^0.11.3
+ version: 0.11.3
vite-plugin-svgr:
specifier: ^4.2.0
version: 4.2.0(typescript@5.3.2)(vite@5.0.2)
@@ -1115,22 +1188,6 @@ packages:
resolution: {integrity: sha512-/62yikz7NLScCGAAST5SHdnjaDJQBDq0M2muyRTpf2VQhw6StBg2ALiu73zSJQ4fMVLA+0uBhBHAle7Wg+2kSg==}
dev: true
- /@alchemy-fr/liform-react@0.9.3(react-redux@5.1.2)(redux-form@7.4.3)(redux@4.2.1):
- resolution: {integrity: sha512-WBlt8F3+CYnGKBdTPVk+MR0/R3OltVtcL4AGTJsixbAxmjGIA4zwQkeZr5FMc9HZ0nimIlNPdOhsBhTdSDaibQ==}
- peerDependencies:
- react-redux: ^5.0.6
- redux: ^3.5.2
- redux-form: ^7.2.0
- dependencies:
- ajv: 5.5.2
- classnames: 2.3.2
- deepmerge: 2.2.1
- prop-types: 15.8.1
- react-redux: 5.1.2(react@18.2.0)(redux@4.2.1)
- redux: 4.2.1
- redux-form: 7.4.3(react-redux@5.1.2)(react@18.2.0)(redux@4.2.1)
- dev: false
-
/@ampproject/remapping@2.2.1:
resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==}
engines: {node: '>=6.0.0'}
@@ -3599,31 +3656,8 @@ packages:
react: 18.2.0
dev: false
- /@mui/base@5.0.0-beta.25(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0):
- resolution: {integrity: sha512-Iiv+IcappRRv6IBlknIVmLkXxfp51NEX1+l9f+dIbBuPU4PaRULegr1lCeHKsC45KU5ruxM5xMg4R/de03aJQg==}
- engines: {node: '>=12.0.0'}
- peerDependencies:
- '@types/react': ^17.0.0 || ^18.0.0
- react: ^17.0.0 || ^18.0.0
- react-dom: ^17.0.0 || ^18.0.0
- peerDependenciesMeta:
- '@types/react':
- optional: true
- dependencies:
- '@babel/runtime': 7.23.4
- '@floating-ui/react-dom': 2.0.4(react-dom@18.2.0)(react@18.2.0)
- '@mui/types': 7.2.10(@types/react@18.2.39)
- '@mui/utils': 5.14.19(@types/react@18.2.39)(react@18.2.0)
- '@popperjs/core': 2.11.8
- '@types/react': 18.2.39
- clsx: 2.0.0
- prop-types: 15.8.1
- react: 18.2.0
- react-dom: 18.2.0(react@18.2.0)
- dev: true
-
- /@mui/base@5.0.0-beta.27(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0):
- resolution: {integrity: sha512-duL37qxihT1N0pW/gyXVezP7SttLkF+cLAs/y6g6ubEFmVadjbnZ45SeF12/vAiKzqwf5M0uFH1cczIPXFZygA==}
+ /@mui/base@5.0.0-beta.28(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-KIoSc5sUFceeCaZTq5MQBapFzhHqMo4kj+4azWaCAjorduhcRQtN+BCgVHmo+gvEjix74bUfxwTqGifnu2fNTg==}
engines: {node: '>=12.0.0'}
peerDependencies:
'@types/react': ^17.0.0 || ^18.0.0
@@ -3636,7 +3670,7 @@ packages:
'@babel/runtime': 7.23.6
'@floating-ui/react-dom': 2.0.4(react-dom@18.2.0)(react@18.2.0)
'@mui/types': 7.2.11(@types/react@18.2.39)
- '@mui/utils': 5.15.0(@types/react@18.2.39)(react@18.2.0)
+ '@mui/utils': 5.15.1(@types/react@18.2.39)(react@18.2.0)
'@popperjs/core': 2.11.8
'@types/react': 18.2.39
clsx: 2.0.0
@@ -3644,14 +3678,10 @@ packages:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
- /@mui/core-downloads-tracker@5.14.19:
- resolution: {integrity: sha512-y4JseIen5pmZs1n9hHy95HKKioKco8f6N2lford2AmjJigVJOv0KsU0qryiCpyuEUZmi/xCduVilHsK9DSkPcA==}
- dev: true
-
- /@mui/core-downloads-tracker@5.15.0:
- resolution: {integrity: sha512-NpGtlHwuyLfJtdrlERXb8qRqd279O0VnuGaZAor1ehdNhUJOD1bSxHDeXKZkbqNpvi50hasFj7lsbTpluworTQ==}
+ /@mui/core-downloads-tracker@5.15.1:
+ resolution: {integrity: sha512-y/nUEsWHyBzaKYp9zLtqJKrLod/zMNEWpMj488FuQY9QTmqBiyUhI2uh7PVaLqLewXRtdmG6JV0b6T5exyuYRw==}
- /@mui/icons-material@5.15.0(@mui/material@5.15.0)(@types/react@18.2.39)(react@18.2.0):
+ /@mui/icons-material@5.15.0(@mui/material@5.15.1)(@types/react@18.2.39)(react@18.2.0):
resolution: {integrity: sha512-zHY6fOkaK7VfhWeyxO8MjO3IAjEYpYMXuqUhX7TkUZJ9+TSH/9dn4ClG4K2j6hdgBU5Yrq2Z/89Bo6BHHp7AdQ==}
engines: {node: '>=12.0.0'}
peerDependencies:
@@ -3663,50 +3693,35 @@ packages:
optional: true
dependencies:
'@babel/runtime': 7.23.6
- '@mui/material': 5.15.0(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0)
+ '@mui/material': 5.15.1(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0)
'@types/react': 18.2.39
react: 18.2.0
dev: false
- /@mui/lab@5.0.0-alpha.156(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.15.0)(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0):
- resolution: {integrity: sha512-OUAckFeqlAG6aIBG1Ud1fDCBqnU1wltWZYHsA7YCGzRBykNzQC/W/dYddp+RJLu0BgYpMiXwPXq2Hg0ERVtaog==}
+ /@mui/icons-material@5.15.1(@mui/material@5.15.1)(@types/react@18.2.39)(react@18.2.0):
+ resolution: {integrity: sha512-VPJdBSyap6uOxCb5BLbWbkvd6aeJCp1pQZm8DcZBITCH0NOSv8Mz9c8Zvo8xr4Od7+xyWHUAgvRSL4047pL2WQ==}
engines: {node: '>=12.0.0'}
peerDependencies:
- '@emotion/react': ^11.5.0
- '@emotion/styled': ^11.3.0
- '@mui/material': '>=5.10.11'
+ '@mui/material': ^5.0.0
'@types/react': ^17.0.0 || ^18.0.0
react: ^17.0.0 || ^18.0.0
- react-dom: ^17.0.0 || ^18.0.0
peerDependenciesMeta:
- '@emotion/react':
- optional: true
- '@emotion/styled':
- optional: true
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.23.6
- '@emotion/react': 11.11.1(@types/react@18.2.39)(react@18.2.0)
- '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.39)(react@18.2.0)
- '@mui/base': 5.0.0-beta.27(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0)
- '@mui/material': 5.15.0(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0)
- '@mui/system': 5.15.0(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.39)(react@18.2.0)
- '@mui/types': 7.2.11(@types/react@18.2.39)
- '@mui/utils': 5.15.0(@types/react@18.2.39)(react@18.2.0)
+ '@mui/material': 5.15.1(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0)
'@types/react': 18.2.39
- clsx: 2.0.0
- prop-types: 15.8.1
react: 18.2.0
- react-dom: 18.2.0(react@18.2.0)
- dev: false
+ dev: true
- /@mui/material@5.14.19(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0):
- resolution: {integrity: sha512-jSPLXst/YPgDGolhiu4rbethKjLVrI1IkoK8YrFUv8ygxDuhQdsE6+ZqjSSRXk3ytTMf6ghPnQ88OFRk4XjpNw==}
+ /@mui/lab@5.0.0-alpha.157(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.15.1)(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-gY7UM2kNSxiVLfsm0o6HG2G5rM2Vr47prJhDCazY+VG/NOSRc8CG7la6dpL9WDTJhotEZdWwfj1FOUxTonmuQA==}
engines: {node: '>=12.0.0'}
peerDependencies:
'@emotion/react': ^11.5.0
'@emotion/styled': ^11.3.0
+ '@mui/material': '>=5.10.11'
'@types/react': ^17.0.0 || ^18.0.0
react: ^17.0.0 || ^18.0.0
react-dom: ^17.0.0 || ^18.0.0
@@ -3718,25 +3733,22 @@ packages:
'@types/react':
optional: true
dependencies:
- '@babel/runtime': 7.23.4
- '@mui/base': 5.0.0-beta.25(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0)
- '@mui/core-downloads-tracker': 5.14.19
- '@mui/system': 5.14.19(@types/react@18.2.39)(react@18.2.0)
- '@mui/types': 7.2.10(@types/react@18.2.39)
- '@mui/utils': 5.14.19(@types/react@18.2.39)(react@18.2.0)
+ '@babel/runtime': 7.23.6
+ '@emotion/react': 11.11.1(@types/react@18.2.39)(react@18.2.0)
+ '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.39)(react@18.2.0)
+ '@mui/base': 5.0.0-beta.28(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0)
+ '@mui/material': 5.15.1(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0)
+ '@mui/system': 5.15.1(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.39)(react@18.2.0)
+ '@mui/types': 7.2.11(@types/react@18.2.39)
+ '@mui/utils': 5.15.1(@types/react@18.2.39)(react@18.2.0)
'@types/react': 18.2.39
- '@types/react-transition-group': 4.4.9
clsx: 2.0.0
- csstype: 3.1.2
prop-types: 15.8.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
- react-is: 18.2.0
- react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0)
- dev: true
- /@mui/material@5.15.0(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0):
- resolution: {integrity: sha512-60CDI/hQNwJv9a3vEZtFG7zz0USdQhVwpBd3fZqrzhuXSdiMdYMaZcCXeX/KMuNq0ZxQEAZd74Pv+gOb408QVA==}
+ /@mui/material@5.15.1(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-WA5DVyvacxDakVyAhNqu/rRT28ppuuUFFw1bLpmRzrCJ4uw/zLTATcd4WB3YbB+7MdZNEGG/SJNWTDLEIyn3xQ==}
engines: {node: '>=12.0.0'}
peerDependencies:
'@emotion/react': ^11.5.0
@@ -3755,13 +3767,13 @@ packages:
'@babel/runtime': 7.23.6
'@emotion/react': 11.11.1(@types/react@18.2.39)(react@18.2.0)
'@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.39)(react@18.2.0)
- '@mui/base': 5.0.0-beta.27(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0)
- '@mui/core-downloads-tracker': 5.15.0
- '@mui/system': 5.15.0(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.39)(react@18.2.0)
+ '@mui/base': 5.0.0-beta.28(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0)
+ '@mui/core-downloads-tracker': 5.15.1
+ '@mui/system': 5.15.1(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.39)(react@18.2.0)
'@mui/types': 7.2.11(@types/react@18.2.39)
- '@mui/utils': 5.15.0(@types/react@18.2.39)(react@18.2.0)
+ '@mui/utils': 5.15.1(@types/react@18.2.39)(react@18.2.0)
'@types/react': 18.2.39
- '@types/react-transition-group': 4.4.9
+ '@types/react-transition-group': 4.4.10
clsx: 2.0.0
csstype: 3.1.2
prop-types: 15.8.1
@@ -3770,8 +3782,8 @@ packages:
react-is: 18.2.0
react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0)
- /@mui/private-theming@5.14.19(@types/react@18.2.39)(react@18.2.0):
- resolution: {integrity: sha512-U9w39VpXLGVM8wZlUU/47YGTsBSk60ZQRRxQZtdqPfN1N7OVllQeN4cEKZKR8PjqqR3aYRcSciQ4dc6CttRoXQ==}
+ /@mui/private-theming@5.15.1(@types/react@18.2.39)(react@18.2.0):
+ resolution: {integrity: sha512-wTbzuy5KjSvCPE9UVJktWHJ0b/tD5biavY9wvF+OpYDLPpdXK52vc1hTDxSbdkHIFMkJExzrwO9GvpVAHZBnFQ==}
engines: {node: '>=12.0.0'}
peerDependencies:
'@types/react': ^17.0.0 || ^18.0.0
@@ -3781,50 +3793,13 @@ packages:
optional: true
dependencies:
'@babel/runtime': 7.23.6
- '@mui/utils': 5.15.0(@types/react@18.2.39)(react@18.2.0)
+ '@mui/utils': 5.15.1(@types/react@18.2.39)(react@18.2.0)
'@types/react': 18.2.39
prop-types: 15.8.1
react: 18.2.0
- dev: true
- /@mui/private-theming@5.15.0(@types/react@18.2.39)(react@18.2.0):
- resolution: {integrity: sha512-7WxtIhXxNek0JjtsYy+ut2LtFSLpsUW5JSDehQO+jF7itJ8ehy7Bd9bSt2yIllbwGjCFowLfYpPk2Ykgvqm1tA==}
- engines: {node: '>=12.0.0'}
- peerDependencies:
- '@types/react': ^17.0.0 || ^18.0.0
- react: ^17.0.0 || ^18.0.0
- peerDependenciesMeta:
- '@types/react':
- optional: true
- dependencies:
- '@babel/runtime': 7.23.6
- '@mui/utils': 5.15.0(@types/react@18.2.39)(react@18.2.0)
- '@types/react': 18.2.39
- prop-types: 15.8.1
- react: 18.2.0
-
- /@mui/styled-engine@5.14.19(react@18.2.0):
- resolution: {integrity: sha512-jtj/Pyn/bS8PM7NXdFNTHWZfE3p+vItO4/HoQbUeAv3u+cnWXcTBGHHY/xdIn446lYGFDczTh1YyX8G4Ts0Rtg==}
- engines: {node: '>=12.0.0'}
- peerDependencies:
- '@emotion/react': ^11.4.1
- '@emotion/styled': ^11.3.0
- react: ^17.0.0 || ^18.0.0
- peerDependenciesMeta:
- '@emotion/react':
- optional: true
- '@emotion/styled':
- optional: true
- dependencies:
- '@babel/runtime': 7.23.6
- '@emotion/cache': 11.11.0
- csstype: 3.1.2
- prop-types: 15.8.1
- react: 18.2.0
- dev: true
-
- /@mui/styled-engine@5.15.0(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0):
- resolution: {integrity: sha512-6NysIsHkuUS2lF+Lzv1jiK3UjBJk854/vKVcJQVGKlPiqNEVZJNlwaSpsaU5xYXxWEZYfbVFSAomLOS/LV/ovQ==}
+ /@mui/styled-engine@5.15.1(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0):
+ resolution: {integrity: sha512-7WDZTJLqGexWDjqE9oAgjU8ak6hEtUw2yQU7SIYID5kLVO2Nj/Wi/KicbLsXnTsJNvSqePIlUIWTBSXwWJCPZw==}
engines: {node: '>=12.0.0'}
peerDependencies:
'@emotion/react': ^11.4.1
@@ -3844,36 +3819,8 @@ packages:
prop-types: 15.8.1
react: 18.2.0
- /@mui/system@5.14.19(@types/react@18.2.39)(react@18.2.0):
- resolution: {integrity: sha512-4e3Q+2nx+vgEsd0h5ftxlZGB7XtkkPos/zWqCqnxUs1l/T70s0lF2YNrWHHdSQ7LgtBu0eQ0qweZG2pR7KwkAw==}
- engines: {node: '>=12.0.0'}
- peerDependencies:
- '@emotion/react': ^11.5.0
- '@emotion/styled': ^11.3.0
- '@types/react': ^17.0.0 || ^18.0.0
- react: ^17.0.0 || ^18.0.0
- peerDependenciesMeta:
- '@emotion/react':
- optional: true
- '@emotion/styled':
- optional: true
- '@types/react':
- optional: true
- dependencies:
- '@babel/runtime': 7.23.4
- '@mui/private-theming': 5.14.19(@types/react@18.2.39)(react@18.2.0)
- '@mui/styled-engine': 5.14.19(react@18.2.0)
- '@mui/types': 7.2.10(@types/react@18.2.39)
- '@mui/utils': 5.14.19(@types/react@18.2.39)(react@18.2.0)
- '@types/react': 18.2.39
- clsx: 2.0.0
- csstype: 3.1.2
- prop-types: 15.8.1
- react: 18.2.0
- dev: true
-
- /@mui/system@5.15.0(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.39)(react@18.2.0):
- resolution: {integrity: sha512-8TPjfTlYBNB7/zBJRL4QOD9kImwdZObbiYNh0+hxvhXr2koezGx8USwPXj8y/JynbzGCkIybkUztCdWlMZe6OQ==}
+ /@mui/system@5.15.1(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.39)(react@18.2.0):
+ resolution: {integrity: sha512-LAnP0ls69rqW9eBgI29phIx/lppv+WDGI7b3EJN7VZIqw0RezA0GD7NRpV12BgEYJABEii6z5Q9B5tg7dsX0Iw==}
engines: {node: '>=12.0.0'}
peerDependencies:
'@emotion/react': ^11.5.0
@@ -3891,27 +3838,16 @@ packages:
'@babel/runtime': 7.23.6
'@emotion/react': 11.11.1(@types/react@18.2.39)(react@18.2.0)
'@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.39)(react@18.2.0)
- '@mui/private-theming': 5.15.0(@types/react@18.2.39)(react@18.2.0)
- '@mui/styled-engine': 5.15.0(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0)
+ '@mui/private-theming': 5.15.1(@types/react@18.2.39)(react@18.2.0)
+ '@mui/styled-engine': 5.15.1(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0)
'@mui/types': 7.2.11(@types/react@18.2.39)
- '@mui/utils': 5.15.0(@types/react@18.2.39)(react@18.2.0)
+ '@mui/utils': 5.15.1(@types/react@18.2.39)(react@18.2.0)
'@types/react': 18.2.39
clsx: 2.0.0
csstype: 3.1.2
prop-types: 15.8.1
react: 18.2.0
- /@mui/types@7.2.10(@types/react@18.2.39):
- resolution: {integrity: sha512-wX1vbDC+lzF7FlhT6A3ffRZgEoKWPF8VqRoTu4lZwouFX2t90KyCMsgepMw5DxLak1BSp/KP86CmtZttikb/gQ==}
- peerDependencies:
- '@types/react': ^17.0.0 || ^18.0.0
- peerDependenciesMeta:
- '@types/react':
- optional: true
- dependencies:
- '@types/react': 18.2.39
- dev: true
-
/@mui/types@7.2.11(@types/react@18.2.39):
resolution: {integrity: sha512-KWe/QTEsFFlFSH+qRYf3zoFEj3z67s+qAuSnMMg+gFwbxG7P96Hm6g300inQL1Wy///gSRb8juX7Wafvp93m3w==}
peerDependencies:
@@ -3922,26 +3858,8 @@ packages:
dependencies:
'@types/react': 18.2.39
- /@mui/utils@5.14.19(@types/react@18.2.39)(react@18.2.0):
- resolution: {integrity: sha512-qAHvTXzk7basbyqPvhgWqN6JbmI2wLB/mf97GkSlz5c76MiKYV6Ffjvw9BjKZQ1YRb8rDX9kgdjRezOcoB91oQ==}
- engines: {node: '>=12.0.0'}
- peerDependencies:
- '@types/react': ^17.0.0 || ^18.0.0
- react: ^17.0.0 || ^18.0.0
- peerDependenciesMeta:
- '@types/react':
- optional: true
- dependencies:
- '@babel/runtime': 7.23.4
- '@types/prop-types': 15.7.11
- '@types/react': 18.2.39
- prop-types: 15.8.1
- react: 18.2.0
- react-is: 18.2.0
- dev: true
-
- /@mui/utils@5.15.0(@types/react@18.2.39)(react@18.2.0):
- resolution: {integrity: sha512-XSmTKStpKYamewxyJ256+srwEnsT3/6eNo6G7+WC1tj2Iq9GfUJ/6yUoB7YXjOD2jTZ3XobToZm4pVz1LBt6GA==}
+ /@mui/utils@5.15.1(@types/react@18.2.39)(react@18.2.0):
+ resolution: {integrity: sha512-V1/d0E3Bju5YdB59HJf2G0tnHrFEvWLN+f8hAXp9+JSNy/LC2zKyqUfPPahflR6qsI681P8G9r4mEZte/SrrYA==}
engines: {node: '>=12.0.0'}
peerDependencies:
'@types/react': ^17.0.0 || ^18.0.0
@@ -4856,6 +4774,25 @@ packages:
- immer
dev: false
+ /@reduxjs/toolkit@2.0.1(react-redux@9.0.4)(react@18.2.0):
+ resolution: {integrity: sha512-fxIjrR9934cmS8YXIGd9e7s1XRsEU++aFc9DVNMFMRTM5Vtsg2DCRMj21eslGtDt43IUf9bJL3h5bwUlZleibA==}
+ peerDependencies:
+ react: ^16.9.0 || ^17.0.0 || ^18
+ react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0
+ peerDependenciesMeta:
+ react:
+ optional: true
+ react-redux:
+ optional: true
+ dependencies:
+ immer: 10.0.3
+ react: 18.2.0
+ react-redux: 9.0.4(@types/react@18.2.38)(react@18.2.0)(redux@4.2.1)
+ redux: 5.0.0
+ redux-thunk: 3.1.0(redux@5.0.0)
+ reselect: 5.0.1
+ dev: false
+
/@remix-run/router@1.11.0:
resolution: {integrity: sha512-BHdhcWgeiudl91HvVa2wxqZjSHbheSgIiDvxrF1VjFzBzpTtuDPkOdOi3Iqvc08kXtFkLjhbS+ML9aM8mJS+wQ==}
engines: {node: '>=14.0.0'}
@@ -7171,6 +7108,11 @@ packages:
- react-dom
dev: true
+ /@types/react-transition-group@4.4.10:
+ resolution: {integrity: sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==}
+ dependencies:
+ '@types/react': 18.2.39
+
/@types/react-transition-group@4.4.9:
resolution: {integrity: sha512-ZVNmWumUIh5NhH8aMD9CR2hdW0fNuYInlocZHaZ+dgk/1K49j1w/HoAuK1ki+pgscQrOFRTlXeoURtuzEkV3dg==}
dependencies:
@@ -7265,6 +7207,10 @@ packages:
resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==}
dev: true
+ /@types/use-sync-external-store@0.0.3:
+ resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==}
+ dev: false
+
/@types/uuid@9.0.7:
resolution: {integrity: sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g==}
dev: true
@@ -7438,7 +7384,7 @@ packages:
peerDependencies:
video.js: ^6 || ^7
dependencies:
- '@babel/runtime': 7.23.4
+ '@babel/runtime': 7.23.6
'@videojs/vhs-utils': 3.0.5
aes-decrypter: 3.1.3
global: 4.4.0
@@ -7452,7 +7398,7 @@ packages:
resolution: {integrity: sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==}
engines: {node: '>=8', npm: '>=5'}
dependencies:
- '@babel/runtime': 7.23.4
+ '@babel/runtime': 7.23.6
global: 4.4.0
url-toolkit: 2.2.5
dev: false
@@ -7460,7 +7406,7 @@ packages:
/@videojs/xhr@2.6.0:
resolution: {integrity: sha512-7J361GiN1tXpm+gd0xz2QWr3xNWBE+rytvo8J3KuggFaLg+U37gZQ2BuPLcnkfGffy2e+ozY70RHC8jt7zjA6Q==}
dependencies:
- '@babel/runtime': 7.23.4
+ '@babel/runtime': 7.23.6
global: 4.4.0
is-function: 1.0.2
dev: false
@@ -7994,7 +7940,7 @@ packages:
/aes-decrypter@3.1.3:
resolution: {integrity: sha512-VkG9g4BbhMBy+N5/XodDeV6F02chEk9IpgRTq/0bS80y4dzy79VH2Gtms02VXomf3HmyRe3yyJYkJ990ns+d6A==}
dependencies:
- '@babel/runtime': 7.23.4
+ '@babel/runtime': 7.23.6
'@videojs/vhs-utils': 3.0.5
global: 4.4.0
pkcs7: 1.0.4
@@ -8038,15 +7984,6 @@ packages:
dependencies:
ajv: 6.12.6
- /ajv@5.5.2:
- resolution: {integrity: sha512-Ajr4IcMXq/2QmMkEmSvxqfLN5zGmJ92gHXAeOXq1OekoH2rfDNsgdDoL2f7QaRCy7G/E6TpxBVdRuNraMztGHw==}
- dependencies:
- co: 4.6.0
- fast-deep-equal: 1.1.0
- fast-json-stable-stringify: 2.1.0
- json-schema-traverse: 0.3.1
- dev: false
-
/ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
dependencies:
@@ -8055,6 +7992,15 @@ packages:
json-schema-traverse: 0.4.1
uri-js: 4.4.1
+ /ajv@8.12.0:
+ resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==}
+ dependencies:
+ fast-deep-equal: 3.1.3
+ json-schema-traverse: 1.0.0
+ require-from-string: 2.0.2
+ uri-js: 4.4.1
+ dev: false
+
/amdefine@1.0.1:
resolution: {integrity: sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==}
engines: {node: '>=0.4.2'}
@@ -9615,6 +9561,7 @@ packages:
/co@4.6.0:
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
+ dev: true
/code-point-at@1.1.0:
resolution: {integrity: sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==}
@@ -10720,7 +10667,7 @@ packages:
/dom-helpers@5.2.1:
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
dependencies:
- '@babel/runtime': 7.23.4
+ '@babel/runtime': 7.23.6
csstype: 3.1.2
/dom-serializer@0.2.2:
@@ -11649,10 +11596,6 @@ packages:
kind-of: 5.1.0
dev: true
- /fast-deep-equal@1.1.0:
- resolution: {integrity: sha512-fueX787WZKCV0Is4/T2cyAdM4+x1S3MXXOAhavE1ys/W42SHAPacLTQhucja22QBYrfGw50M2sRiXPtTGv9Ymw==}
- dev: false
-
/fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
@@ -12047,7 +11990,7 @@ packages:
fetch-blob: 3.2.0
dev: true
- /formik-material-ui@4.0.0-alpha.2(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.15.0)(formik@2.4.5)(react@18.2.0)(tiny-warning@1.0.3):
+ /formik-material-ui@4.0.0-alpha.2(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.15.1)(formik@2.4.5)(react@18.2.0)(tiny-warning@1.0.3):
resolution: {integrity: sha512-jGKCUMpexQiU3NN+vYx1MgsgJFLaHnOVJAdKSN7IThKtrWs7t7m6Ft7AyRl/ZDEztKo/sWunlGJvaQJbQBBH9A==}
deprecated: Package renamed to formik-mui
peerDependencies:
@@ -12060,7 +12003,7 @@ packages:
dependencies:
'@emotion/react': 11.11.1(@types/react@18.2.39)(react@18.2.0)
'@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.39)(react@18.2.0)
- '@mui/material': 5.15.0(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0)
+ '@mui/material': 5.15.1(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0)
formik: 2.4.5(react@18.2.0)
react: 18.2.0
tiny-warning: 1.0.3
@@ -12915,10 +12858,6 @@ packages:
requiresBuild: true
dev: true
- /hoist-non-react-statics@2.5.5:
- resolution: {integrity: sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==}
- dev: false
-
/hoist-non-react-statics@3.3.2:
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
dependencies:
@@ -13210,7 +13149,6 @@ packages:
resolution: {integrity: sha512-peTvdT+Lma+o0LfLFD7IC2M37N9DJ04dH0IJYOyOHRhDfLo6nK36v7LkrQH35C2l8NHiiXZqGirhKESlEb/5PA==}
dependencies:
'@babel/runtime': 7.23.6
- dev: true
/iconv-lite@0.4.24:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
@@ -13237,6 +13175,10 @@ packages:
engines: {node: '>= 4'}
dev: true
+ /immer@10.0.3:
+ resolution: {integrity: sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A==}
+ dev: false
+
/immutable@4.3.4:
resolution: {integrity: sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==}
@@ -14817,13 +14759,13 @@ packages:
/json-parse-even-better-errors@2.3.1:
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
- /json-schema-traverse@0.3.1:
- resolution: {integrity: sha512-4JD/Ivzg7PoW8NzdrBSr3UFwC9mHgvI7Z6z3QGBsSHgKaRTUDmyZAAKJo2UbG1kUVfS9WS8bi36N49U1xw43DA==}
- dev: false
-
/json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
+ /json-schema-traverse@1.0.0:
+ resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
+ dev: false
+
/json-schema@0.4.0:
resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==}
@@ -15466,7 +15408,7 @@ packages:
/m3u8-parser@4.8.0:
resolution: {integrity: sha512-UqA2a/Pw3liR6Df3gwxrqghCP17OpPlQj6RBPLYygf/ZSQ4MoSgvdvhvt35qV+3NaaA0FSZx93Ix+2brT1U7cA==}
dependencies:
- '@babel/runtime': 7.23.4
+ '@babel/runtime': 7.23.6
'@videojs/vhs-utils': 3.0.5
global: 4.4.0
dev: false
@@ -16001,7 +15943,7 @@ packages:
resolution: {integrity: sha512-fwBebvpyPUU8bOzvhX0VQZgSohncbgYwUyJJoTSNpmy7ccD2ryiCvM7oRkn/xQH5cv73/xU7rJSNCLjdGFor0Q==}
hasBin: true
dependencies:
- '@babel/runtime': 7.23.4
+ '@babel/runtime': 7.23.6
'@videojs/vhs-utils': 3.0.5
'@xmldom/xmldom': 0.8.10
global: 4.4.0
@@ -16042,7 +15984,7 @@ packages:
engines: {node: '>=8', npm: '>=5'}
hasBin: true
dependencies:
- '@babel/runtime': 7.23.4
+ '@babel/runtime': 7.23.6
global: 4.4.0
dev: false
@@ -17294,6 +17236,10 @@ packages:
pump: 2.0.1
dev: true
+ /punycode@1.4.1:
+ resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==}
+ dev: false
+
/punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
@@ -17334,7 +17280,6 @@ packages:
engines: {node: '>=0.6'}
dependencies:
side-channel: 1.0.4
- dev: true
/qs@6.5.3:
resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==}
@@ -17756,7 +17701,7 @@ packages:
react-native:
optional: true
dependencies:
- '@babel/runtime': 7.23.4
+ '@babel/runtime': 7.23.6
html-parse-stringify: 3.0.1
i18next: 21.10.0
react: 18.2.0
@@ -17776,12 +17721,11 @@ packages:
react-native:
optional: true
dependencies:
- '@babel/runtime': 7.23.4
+ '@babel/runtime': 7.23.6
html-parse-stringify: 3.0.1
i18next: 23.7.7
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
- dev: true
/react-icons@4.12.0(react@18.2.0):
resolution: {integrity: sha512-IBaDuHiShdZqmfc/TwHu6+d6k2ltNCf3AszxNmjJc1KUfXdEeRJOKyNvLmAHaarhzGmTSVygNdyu8/opXv2gaw==}
@@ -17942,7 +17886,7 @@ packages:
react: '>=16.3.0'
react-dom: '>=16.3.0'
dependencies:
- '@babel/runtime': 7.23.4
+ '@babel/runtime': 7.23.6
'@popperjs/core': 2.11.8
'@restart/hooks': 0.4.11(react@18.2.0)
'@types/warning': 3.0.3
@@ -18007,21 +17951,26 @@ packages:
resolution: {integrity: sha512-1tKOwxFn3dXVomH6pM9IkLkq2Y8oh+fh/lYW3MJ/B03URswUTqttgckOlbxY2XHF3vPG6uanSc4dVsLW/wk3wQ==}
dev: false
- /react-redux@5.1.2(react@18.2.0)(redux@4.2.1):
- resolution: {integrity: sha512-Ns1G0XXc8hDyH/OcBHOxNgQx9ayH3SPxBnFCOidGKSle8pKihysQw2rG/PmciUQRoclhVBO8HMhiRmGXnDja9Q==}
+ /react-redux@9.0.4(@types/react@18.2.38)(react@18.2.0)(redux@4.2.1):
+ resolution: {integrity: sha512-9J1xh8sWO0vYq2sCxK2My/QO7MzUMRi3rpiILP/+tDr8krBHixC6JMM17fMK88+Oh3e4Ae6/sHIhNBgkUivwFA==}
peerDependencies:
- react: ^0.14.0 || ^15.0.0-0 || ^16.0.0-0
- redux: ^2.0.0 || ^3.0.0 || ^4.0.0-0
+ '@types/react': ^18.2.25
+ react: ^18.0
+ react-native: '>=0.69'
+ redux: ^5.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ react-native:
+ optional: true
+ redux:
+ optional: true
dependencies:
- '@babel/runtime': 7.23.4
- hoist-non-react-statics: 3.3.2
- invariant: 2.2.4
- loose-envify: 1.4.0
- prop-types: 15.8.1
+ '@types/react': 18.2.38
+ '@types/use-sync-external-store': 0.0.3
react: 18.2.0
- react-is: 16.13.1
- react-lifecycles-compat: 3.0.4
redux: 4.2.1
+ use-sync-external-store: 1.2.0(react@18.2.0)
dev: false
/react-refresh@0.14.0:
@@ -18404,30 +18353,47 @@ packages:
esprima: 4.0.1
dev: true
- /redux-form@7.4.3(react-redux@5.1.2)(react@18.2.0)(redux@4.2.1):
- resolution: {integrity: sha512-h2LEGdEQ9XaX2wXZnf6zIUFSETk4jDqG4NUKwqkSfOJZDxT3H2hJmKVZI76j/YsE/8E3eY6yPoAENCxjLi1p+Q==}
+ /redux-form@8.3.10(react-redux@9.0.4)(react@18.2.0)(redux@4.2.1):
+ resolution: {integrity: sha512-Eeog8dJYUxCSZI/oBoy7VkprvMjj1lpUnHa3LwjVNZvYDNeiRZMoZoaAT+6nlK2YQ4aiBopKUMiLe4ihUOHCGg==}
+ engines: {node: '>=8.10'}
peerDependencies:
- react: ^15.0.0-0 || ^16.0.0-0
- react-redux: ^4.3.0 || ^5.0.0
- redux: ^3.0.0 || ^4.0.0
+ immutable: ^3.8.2 || ^4.0.0
+ react: ^16.4.2 || ^17.0.0 || ^18.0.0
+ react-redux: ^6.0.1 || ^7.0.0 || ^8.0.0
+ redux: ^3.7.2 || ^4.0.0
+ peerDependenciesMeta:
+ immutable:
+ optional: true
dependencies:
+ '@babel/runtime': 7.23.6
es6-error: 4.1.1
- hoist-non-react-statics: 2.5.5
+ hoist-non-react-statics: 3.3.2
invariant: 2.2.4
is-promise: 2.2.2
lodash: 4.17.21
- lodash-es: 4.17.21
prop-types: 15.8.1
react: 18.2.0
- react-lifecycles-compat: 3.0.4
- react-redux: 5.1.2(react@18.2.0)(redux@4.2.1)
+ react-is: 16.13.1
+ react-redux: 9.0.4(@types/react@18.2.38)(react@18.2.0)(redux@4.2.1)
redux: 4.2.1
dev: false
+ /redux-thunk@3.1.0(redux@5.0.0):
+ resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==}
+ peerDependencies:
+ redux: ^5.0.0
+ dependencies:
+ redux: 5.0.0
+ dev: false
+
/redux@4.2.1:
resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==}
dependencies:
- '@babel/runtime': 7.23.4
+ '@babel/runtime': 7.23.6
+ dev: false
+
+ /redux@5.0.0:
+ resolution: {integrity: sha512-blLIYmYetpZMET6Q6uCY7Jtl/Im5OBldy+vNPauA8vvsdqyt66oep4EUpAMWNHauTC6xa9JuRPhRB72rY82QGA==}
dev: false
/regenerate-unicode-properties@10.1.1:
@@ -18663,6 +18629,11 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
+ /require-from-string@2.0.2:
+ resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
+ engines: {node: '>=0.10.0'}
+ dev: false
+
/require-main-filename@1.0.1:
resolution: {integrity: sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==}
dev: true
@@ -18675,6 +18646,10 @@ packages:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
dev: false
+ /reselect@5.0.1:
+ resolution: {integrity: sha512-D72j2ubjgHpvuCiORWkOUxndHJrxDaSolheiz5CO+roz8ka97/4msh2E8F5qay4GawR5vzBt5MkbDHT+Rdy/Wg==}
+ dev: false
+
/resize-observer-polyfill@1.5.0:
resolution: {integrity: sha512-M2AelyJDVR/oLnToJLtuDJRBBWUGUvvGigj1411hXhAdyFWqMaqHp7TixW3FpiLuVaikIcR1QL+zqoJoZlOgpg==}
dev: false
@@ -20934,7 +20909,7 @@ packages:
peerDependencies:
react: '>=15.0.0'
dependencies:
- '@babel/runtime': 7.23.4
+ '@babel/runtime': 7.23.6
'@types/react': 18.2.39
invariant: 2.2.4
react: 18.2.0
@@ -21156,6 +21131,13 @@ packages:
resolution: {integrity: sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg==}
dev: false
+ /url@0.11.3:
+ resolution: {integrity: sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw==}
+ dependencies:
+ punycode: 1.4.1
+ qs: 6.11.2
+ dev: false
+
/use-callback-ref@1.3.0(@types/react@18.2.38)(react@18.2.0):
resolution: {integrity: sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w==}
engines: {node: '>=10'}
@@ -22311,7 +22293,7 @@ packages:
/yup@0.27.0:
resolution: {integrity: sha512-v1yFnE4+u9za42gG/b/081E7uNW9mUj3qtkmelLbW5YPROZzSH/KUUyJu9Wt8vxFJcT9otL/eZopS0YK1L5yPQ==}
dependencies:
- '@babel/runtime': 7.23.4
+ '@babel/runtime': 7.23.6
fn-name: 2.0.1
lodash: 4.17.21
property-expr: 1.5.1
diff --git a/uploader/client/config-compiler.js b/uploader/client/config-compiler.js
index b12430ba5..94176d47b 100644
--- a/uploader/client/config-compiler.js
+++ b/uploader/client/config-compiler.js
@@ -51,7 +51,7 @@
clientId: env.CLIENT_ID,
devMode: castBoolean(env.DEV_MODE),
displayServicesMenu: castBoolean(env.DISPLAY_SERVICES_MENU),
- dashboardBaseUrl: env.DASHBOARD_URL,
+ dashboardBaseUrl: env.DASHBOARD_CLIENT_URL,
allowedTypes: normalizeTypes(env.ALLOWED_FILE_TYPES),
appId: env.APP_ID || 'uploader',
sentryDsn: env.SENTRY_DSN,
diff --git a/uploader/client/package.json b/uploader/client/package.json
index d686ed5a3..41082af41 100644
--- a/uploader/client/package.json
+++ b/uploader/client/package.json
@@ -12,13 +12,14 @@
"translations-scan": "i18next-scanner --config i18next-scanner.config.js src/**/*.{js,jsx}"
},
"dependencies": {
- "@alchemy-fr/liform-react": "^0.9.3",
+ "@reduxjs/toolkit": "^2.0.1",
+ "@alchemy/liform-react": "workspace:*",
"@alchemy/api": "workspace:*",
- "@alchemy/core": "workspace:*",
"@alchemy/auth": "workspace:*",
- "@alchemy/react-auth": "workspace:*",
+ "@alchemy/core": "workspace:*",
"@alchemy/navigation": "workspace:*",
"@alchemy/phrasea-ui": "workspace:*",
+ "@alchemy/react-auth": "workspace:*",
"@alchemy/react-ps": "workspace:*",
"axios": "^1.6.2",
"bootstrap": "^4.3.1",
@@ -26,7 +27,6 @@
"i18next": "^21.8.0",
"i18next-browser-languagedetector": "^6.1.2",
"i18next-xhr-backend": "^3.1.2",
- "react-toastify": "^9.1.3",
"react": "^18.2.0",
"react-bootstrap": "^1.0.1",
"react-burger-menu": "^2.6.8",
@@ -34,9 +34,11 @@
"react-dropzone": "^14.2.1",
"react-i18next": "^11.15.1",
"react-loader-spinner": "^3.1.14",
- "react-redux": "^5.1",
- "redux": "^4.0.1",
- "redux-form": "^7.4.2",
+ "react-redux": "^9.0.4",
+ "react-toastify": "^9.1.3",
+ "redux": "^4.2.1",
+ "redux-form": "^8.3.10",
+ "url": "^0.11.3",
"vite-plugin-svgr": "^4.2.0"
},
"devDependencies": {
diff --git a/uploader/client/src/App.tsx b/uploader/client/src/App.tsx
index 5e705ce4a..5fecf00a0 100644
--- a/uploader/client/src/App.tsx
+++ b/uploader/client/src/App.tsx
@@ -3,7 +3,7 @@ import {routes} from './routes';
import './scss/App.scss';
import Menu from './components/Menu';
import RouteProxy from './components/RouteProxy';
-import {PropsWithChildren} from "react";
+import {PropsWithChildren} from 'react';
type Props = {};
@@ -20,7 +20,9 @@ export default function App({}: Props) {
}
function Wrapper({children}: PropsWithChildren<{}>) {
- return
-
-
+ return (
+
+
+
+ );
}
diff --git a/uploader/client/src/Root.tsx b/uploader/client/src/Root.tsx
index 2f6dbf60b..808f4b1e7 100644
--- a/uploader/client/src/Root.tsx
+++ b/uploader/client/src/Root.tsx
@@ -12,9 +12,7 @@ export default function Root({}: Props) {
return (
<>
-
+
diff --git a/uploader/client/src/components/AssetForm.jsx b/uploader/client/src/components/AssetForm.jsx
index b3dbc5757..1fe5c40f6 100644
--- a/uploader/client/src/components/AssetForm.jsx
+++ b/uploader/client/src/components/AssetForm.jsx
@@ -86,7 +86,7 @@ export default class AssetForm extends Component {
}
if (r.errors && Object.keys(r.errors).length > 0) {
- const {errors} = r.body;
+ const {errors} = r;
const errs = {};
Object.keys(errors).forEach(i => {
diff --git a/uploader/client/src/components/AssetLiForm.jsx b/uploader/client/src/components/AssetLiForm.jsx
index f0311f396..9cdf1ace3 100644
--- a/uploader/client/src/components/AssetLiForm.jsx
+++ b/uploader/client/src/components/AssetLiForm.jsx
@@ -1,8 +1,9 @@
import React from 'react';
-import {createStore, combineReducers} from 'redux';
+import {combineReducers} from 'redux';
import {reducer as formReducer} from 'redux-form';
import {Provider} from 'react-redux';
-import Liform, {renderField, DefaultTheme} from '@alchemy-fr/liform-react';
+import Liform, {renderField, DefaultTheme} from '@alchemy/liform-react';
+import {configureStore} from '@reduxjs/toolkit';
const BaseForm = props => {
const {schema, handleSubmit, theme, error, submitting, onCancel} = props;
@@ -36,7 +37,7 @@ const BaseForm = props => {
const AssetLiForm = props => {
const reducer = combineReducers({form: formReducer});
- const store = createStore(reducer);
+ const store = configureStore({reducer});
const initialValues = {};
diff --git a/uploader/client/src/components/RouteProxy.tsx b/uploader/client/src/components/RouteProxy.tsx
index ff11fbdcd..0dff57273 100644
--- a/uploader/client/src/components/RouteProxy.tsx
+++ b/uploader/client/src/components/RouteProxy.tsx
@@ -19,5 +19,5 @@ export default function RouteProxy({
return <>>;
}
- return
+ return ;
}
diff --git a/uploader/client/src/index.tsx b/uploader/client/src/index.tsx
index c77d970c8..7cb4a4907 100644
--- a/uploader/client/src/index.tsx
+++ b/uploader/client/src/index.tsx
@@ -2,18 +2,17 @@ import ReactDOM from 'react-dom/client';
import './scss/index.scss';
import './locales/i18n';
import Root from './Root.tsx';
-import React from 'react';
import {DashboardMenu} from '@alchemy/react-ps';
-import config from "./config";
-import {initSentry} from '@alchemy/core'
+import config from './config';
+import {initSentry} from '@alchemy/core';
initSentry(config);
ReactDOM.createRoot(document.getElementById('root')!).render(
-
+ <>
{config.displayServicesMenu && (
)}
-
+ >
);
diff --git a/uploader/client/vite.config.ts b/uploader/client/vite.config.ts
index e6dfbf1de..0ad0e531e 100644
--- a/uploader/client/vite.config.ts
+++ b/uploader/client/vite.config.ts
@@ -21,4 +21,9 @@ export default defineConfig({
port: 3000,
host: '0.0.0.0',
},
+ resolve: {
+ alias: {
+ path: "url",
+ },
+ },
});