Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/1342 rejseplanen rewrite to fit new api WIP #243

Merged
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

- [#242](https://github.com/os2display/display-admin-client/pull/243)
- Add entry in example config for midttrafik api key
- Clean up multi select component a bit, replace reduce with Map logic
- Make the station selector call new api
- Add config to context in app.jsx

-


## [2.0.2] - 2024-04-25

- [#242](https://github.com/os2display/display-admin-client/pull/242)
Expand Down
1 change: 1 addition & 0 deletions public/example_config.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"api": "/",
"touchButtonRegions": false,
"rejseplanenApiKey": "abc",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be added to the infrastructure templates.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default value should be null

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is "the infrastructure templates"?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"loginMethods": [
{
"type": "oidc",
Expand Down
9 changes: 9 additions & 0 deletions src/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import "./app.scss";
import ActivationCodeList from "./components/activation-code/activation-code-list";
import ActivationCodeCreate from "./components/activation-code/activation-code-create";
import ActivationCodeActivate from "./components/activation-code/activation-code-activate";
import ConfigLoader from "./config-loader";

/**
* App component.
Expand All @@ -49,6 +50,7 @@ import ActivationCodeActivate from "./components/activation-code/activation-code
*/
function App() {
const [authenticated, setAuthenticated] = useState();
const [config, setConfig] = useState();
const [selectedTenant, setSelectedTenant] = useState();
const [accessConfig, setAccessConfig] = useState();
const [tenants, setTenants] = useState();
Expand All @@ -63,6 +65,7 @@ function App() {
const userStore = {
authenticated: { get: authenticated, set: setAuthenticated },
accessConfig: { get: accessConfig, set: setAccessConfig },
config,
tenants: { get: tenants, set: setTenants },
selectedTenant: { get: selectedTenant, set: setSelectedTenant },
userName: { get: userName, set: setUserName },
Expand All @@ -76,6 +79,12 @@ function App() {
isPublished: { get: isPublished, set: setIsPublished },
};

useEffect(() => {
ConfigLoader.loadConfig().then((loadedConfig) => {
setConfig(loadedConfig);
});
}, []);

const handleReauthenticate = () => {
localStorage.removeItem(localStorageKeys.API_TOKEN);
localStorage.removeItem(localStorageKeys.API_REFRESH_TOKEN);
Expand Down
2 changes: 1 addition & 1 deletion src/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ body,
display: flex;
justify-content: center;
padding: 5em;
z-index: 10;
z-index: 1021;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid this:

image

.spinner-container {
display: flex;
Expand Down
31 changes: 23 additions & 8 deletions src/components/slide/content/station/station-selector.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { React, useState, useEffect } from "react";
import { React, useState, useEffect, useContext } from "react";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
import MultiSelectComponent from "../../../util/forms/multiselect-dropdown/multi-dropdown";
import { displayError } from "../../../util/list/toast-component/display-toast";

import userContext from "../../../../context/user-context";
/**
* A multiselect and table for groups.
*
Expand All @@ -23,13 +23,15 @@ function StationSelector({
const { t } = useTranslation("common", { keyPrefix: "station-selector" });
const [data, setData] = useState([]);
const [searchText, setSearchText] = useState("");
const { config } = useContext(userContext);

/**
* Adds group to list of groups.
*
* @param {object} props - The props.
* @param {object} props.target - The target.
*/
const handleAdd = ({ target }) => {
const handleSelect = ({ target }) => {
const { value, id: localId } = target;
onChange({
target: { id: localId, value },
Expand All @@ -44,15 +46,29 @@ function StationSelector({
const onFilter = (filter) => {
setSearchText(filter);
};
/**
* Map the data recieved from the midttrafik api.
*
* @param {object} locationData
* @returns {object} The mapped data.
*/
const mapLocationData = (locationData) => {
return locationData.map((location) => ({
id: location.StopLocation.extId,
name: location.StopLocation.name,
}));
};

useEffect(() => {
fetch(
`https://xmlopen.rejseplanen.dk/bin/rest.exe/location?input=user%20i${searchText}?&format=json`
`https://www.rejseplanen.dk/api/location.name?accessId=${
config.rejseplanenApiKey || ""
sinejespersen marked this conversation as resolved.
Show resolved Hide resolved
tuj marked this conversation as resolved.
Show resolved Hide resolved
}&format=json&input=${searchText}`
)
.then((response) => response.json())
.then((rpData) => {
if (rpData?.LocationList?.StopLocation) {
setData(rpData.LocationList.StopLocation);
if (rpData?.stopLocationOrCoordLocation) {
setData(mapLocationData(rpData.stopLocationOrCoordLocation));
}
})
.catch((er) => {
Expand All @@ -66,8 +82,7 @@ function StationSelector({
<>
<MultiSelectComponent
options={data}
singleSelect
handleSelection={handleAdd}
handleSelection={handleSelect}
name={name}
selected={inputValue || []}
filterCallback={onFilter}
Expand Down
11 changes: 4 additions & 7 deletions src/components/slide/slide-form.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { React, useEffect, useState, Fragment } from "react";
import { React, useEffect, useState, Fragment, useContext } from "react";
import { Button, Row, Col } from "react-bootstrap";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
Expand All @@ -21,8 +21,8 @@ import FeedSelector from "./content/feed-selector";
import RadioButtons from "../util/forms/radio-buttons";
import SelectPlaylistsTable from "../util/multi-and-table/select-playlists-table";
import localStorageKeys from "../util/local-storage-keys";
import ConfigLoader from "../../config-loader";
import { displayError } from "../util/list/toast-component/display-toast";
import userContext from "../../context/user-context";
import "./slide-form.scss";

/**
Expand Down Expand Up @@ -61,6 +61,8 @@ function SlideForm({
}) {
const { t } = useTranslation("common");
const navigate = useNavigate();
const { config } = useContext(userContext);

const [showPreview, setShowPreview] = useState(false);
const [previewLayout, setPreviewLayout] = useState("horizontal");
const [previewOverlayVisible, setPreviewOverlayVisible] = useState(false);
Expand All @@ -70,7 +72,6 @@ function SlideForm({
const [searchTextTheme, setSearchTextTheme] = useState("");
const [selectedTemplates, setSelectedTemplates] = useState([]);
const [themesOptions, setThemesOptions] = useState();
const [config, setConfig] = useState({});

// Load templates.
const { data: templates, isLoading: loadingTemplates } =
Expand Down Expand Up @@ -102,10 +103,6 @@ function SlideForm({
useEffect(() => {
window.addEventListener("keydown", downHandler);

ConfigLoader.loadConfig().then((loadedConfig) => {
setConfig(loadedConfig);
});

// Remove event listeners on cleanup
return () => {
window.removeEventListener("keydown", downHandler);
Expand Down
94 changes: 57 additions & 37 deletions src/components/util/forms/multiselect-dropdown/multi-dropdown.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,37 +49,47 @@ function MultiSelectComponent({
const nothingSelectedLabel =
noSelectedString || t("multi-dropdown.nothing-selected");

/** Map data to fit component. */
useEffect(() => {
const localMappedOptions =
options?.map((item) => {
return {
label: item.title || item.name,
value: item["@id"] || item.id,
disabled: false,
};
}) ?? [];
let localMappedSelected = [];
/**
* @param {Array} arrayWithDuplicates - Array of objects to make unique
* @param {string} key - The key to make array unique by.
* @returns {Array} Unique array
*/
function removeDuplicatesByKey(arrayWithDuplicates, key) {
return [
...new Map(arrayWithDuplicates.map((item) => [item[key], item])).values(),
];
}

if (selected.length > 0) {
localMappedSelected = selected.map((item) => {
/**
* @param {Array} dataToMap - The data to map to {label, value, disabled}
* @returns {Array} An array of {label, value, disabled}
*/
function mapDataToFitMultiselect(dataToMap) {
return (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why ( ) around the return value?
It is only needed for jsx

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the linter does it

dataToMap.map((item) => {
return {
label: item.title || item.name,
value: item["@id"] || item.id,
disabled: false,
};
});
}
}) ?? []
);
}

/** Map data to fit component. */
useEffect(() => {
const localMappedOptions =
options.length > 0 ? mapDataToFitMultiselect(options) : [];

const localMappedSelected =
selected.length > 0 ? mapDataToFitMultiselect(selected) : [];

const optionsWithSelected = Object.values(
[...localMappedOptions, ...localMappedSelected].reduce((a, c) => {
const aCopy = { ...a };
aCopy[c.value] = c;
return aCopy;
}, {})
const optionsWithSelected = removeDuplicatesByKey(
[...localMappedOptions, ...localMappedSelected],
"value"
);
setMappedOptions(optionsWithSelected);

setMappedOptions(optionsWithSelected);
setMappedSelected(localMappedSelected);
}, [selected, selected.length, options]);

Expand All @@ -103,29 +113,39 @@ function MultiSelectComponent({
);
};

/**
* Filter to replace the default filter in multi-select. It matches the label name.
*
* @param {Array} multiselectData Data from the multiselect component
* @returns {Array} Array of selected values without duplicates
*/
const addOrRemoveNewEntryToSelected = (multiselectData) => {
let selectedOptions = [];
const idsOfSelectedEntries = multiselectData.map(({ value }) => value);

selectedOptions = removeDuplicatesByKey(
[...selected, ...options].filter((option) =>
idsOfSelectedEntries.includes(option["@id"] || option.id)
),
"id"
);

if (singleSelect) {
selectedOptions = [selectedOptions[selectedOptions.length - 1]];
}

return selectedOptions;
};

/**
* A callback on changed data.
*
* @param {Array} data The data to call back with
*/
const changeData = (data) => {
let selectedOptions = [];

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New lines around if statements increase the readability of the code.

if (data.length > 0) {
const ids = data.map(({ value }) => value);
selectedOptions = Object.values(
[...selected, ...options]
.filter((option) => ids.includes(option["@id"] || option.id))
.reduce((a, c) => {
const aCopy = { ...a };
aCopy[c["@id"] || c.id] = c;
return aCopy;
}, {})
);

if (singleSelect) {
selectedOptions = [selectedOptions[selectedOptions.length - 1]];
}
selectedOptions = addOrRemoveNewEntryToSelected(data);
}

const target = { value: selectedOptions, id: name };
Expand Down
Loading