diff --git a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/JSLang.java b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/JSLang.java
index 7999d6290..e9a1931b8 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/JSLang.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/JSLang.java
@@ -56,6 +56,10 @@ public enum JSLang implements Lang {
QUERY_ACTIVITY_ON("html.query.title.activityOnDate", "Activity on "),
QUERY_ARE("html.query.generic.are", "`are`"),
QUERY_SESSIONS_WITHIN_VIEW("html.query.title.sessionsWithinView", "Sessions within view"),
+ QUERY_HAS_PLUGIN_BOOLEAN_VALUE("html.query.filter.hasPluginBooleanValue.name", "Has plugin boolean value"),
+ QUERY_HAVE_PLUGIN_BOOLEAN_VALUE("html.query.filter.hasPluginBooleanValue.text", "have Plugin boolean value"),
+ QUERY_HAS_PLAYED_ON_SERVERS("html.query.filter.hasPlayedOnServers.name", "Has played on one of servers"),
+ QUERY_HAVE_PLAYED_ON_SERVERS("html.query.filter.hasPlayedOnServers.text", "have played on at least one of"),
FILTER_GROUP("html.query.filter.pluginGroup.name", "Group: "),
FILTER_ALL_PLAYERS("html.query.filter.generic.allPlayers", "All players"),
diff --git a/Plan/react/dashboard/src/components/cards/query/FilterDropdown.js b/Plan/react/dashboard/src/components/cards/query/FilterDropdown.js
new file mode 100644
index 000000000..e15fb2006
--- /dev/null
+++ b/Plan/react/dashboard/src/components/cards/query/FilterDropdown.js
@@ -0,0 +1,68 @@
+import React from 'react';
+import DropdownToggle from "react-bootstrap-v5/lib/esm/DropdownToggle";
+import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
+import DropdownMenu from "react-bootstrap-v5/lib/esm/DropdownMenu";
+import DropdownItem from "react-bootstrap-v5/lib/esm/DropdownItem";
+import {Dropdown} from "react-bootstrap-v5";
+import {faPlus} from "@fortawesome/free-solid-svg-icons";
+import {useTranslation} from "react-i18next";
+import Scrollable from "../../Scrollable";
+
+const FilterDropdown = ({filterOptions, filters, setFilters}) => {
+ const {t} = useTranslation();
+
+ const addFilter = filter => {
+ setFilters([...filters, filter])
+ }
+
+ const getReadableFilterName = filter => {
+ if (filter.kind.startsWith("pluginGroups-")) {
+ return t('html.query.filter.pluginGroup.name') + filter.kind.substring(13);
+ }
+ switch (filter.kind) {
+ case "allPlayers":
+ return t('html.query.filter.generic.allPlayers')
+ case "activityIndexNow":
+ return t('html.query.filter.title.activityGroup');
+ case "banned":
+ return t('html.query.filter.banStatus.name');
+ case "operators":
+ return t('html.query.filter.operatorStatus.name');
+ case "joinAddresses":
+ return t('html.label.joinAddresses');
+ case "geolocations":
+ return t('html.label.geolocations');
+ case "playedBetween":
+ return t('html.query.filter.playedBetween.text');
+ case "registeredBetween":
+ return t('html.query.filter.registeredBetween.text');
+ case "pluginsBooleanGroups":
+ return t('html.query.filter.hasPluginBooleanValue.name');
+ case "playedOnServer":
+ return t('html.query.filter.hasPlayedOnServers.name');
+ default:
+ return filter.kind;
+ }
+ };
+
+ return (
+
+
+ {t('html.query.filters.add')}
+
+
+
+ {t('html.query.filters.add')}
+
+ {filterOptions.map((option, i) => (
+ addFilter(option)}>
+ {getReadableFilterName(option)}
+
+ ))}
+
+
+
+ )
+};
+
+export default FilterDropdown
\ No newline at end of file
diff --git a/Plan/react/dashboard/src/components/cards/query/FilterList.js b/Plan/react/dashboard/src/components/cards/query/FilterList.js
new file mode 100644
index 000000000..6314276e4
--- /dev/null
+++ b/Plan/react/dashboard/src/components/cards/query/FilterList.js
@@ -0,0 +1,45 @@
+import React from 'react';
+import Filter from "./filter/Filter";
+
+const FilterList = ({filters, setFilters, setAsInvalid, setAsValid}) => {
+ const updateFilterOptions = (index, newOptions) => {
+ filters[index] = newOptions;
+ setFilters(filters);
+ }
+
+ const removeFilter = index => {
+ setFilters(filters.filter((f, i) => i !== index));
+ }
+
+ const moveUp = index => {
+ if (index === 0) {
+ return;
+ }
+ [filters[index - 1], filters[index]] = [filters[index], filters[index - 1]];
+ setFilters(filters);
+ }
+
+ const moveDown = index => {
+ if (index === filters.length - 1) {
+ return;
+ }
+ [filters[index], filters[index + 1]] = [filters[index + 1], filters[index]];
+ setFilters(filters);
+ }
+
+ return (
+
+ {filters.map((filter, i) => -
+ updateFilterOptions(i, newOptions)}
+ removeFilter={() => removeFilter(i)}
+ moveUp={() => moveUp(i)}
+ moveDown={() => moveDown(i)}
+ setAsInvalid={setAsInvalid} setAsValid={setAsValid}
+ />
+
)}
+
+ )
+};
+
+export default FilterList
\ No newline at end of file
diff --git a/Plan/react/dashboard/src/components/cards/query/QueryOptionsCard.js b/Plan/react/dashboard/src/components/cards/query/QueryOptionsCard.js
index 582c31abf..3e5c772bc 100644
--- a/Plan/react/dashboard/src/components/cards/query/QueryOptionsCard.js
+++ b/Plan/react/dashboard/src/components/cards/query/QueryOptionsCard.js
@@ -13,10 +13,12 @@ import PlayersOnlineGraph from "../../graphs/PlayersOnlineGraph";
import Highcharts from "highcharts/highstock";
import MultiSelect from "../../input/MultiSelect";
import CollapseWithButton from "../../layout/CollapseWithButton";
+import FilterDropdown from "./FilterDropdown";
+import FilterList from "./FilterList";
const parseTime = (dateString, timeString) => {
const d = dateString.match(
- /^(0\d|\d{2})[\/|\-]?(0\d|\d{2})[\/|\-]?(\d{4,5})$/
+ /^(0\d|\d{2})[/|-]?(0\d|\d{2})[/|-]?(\d{4,5})$/
);
const t = timeString.match(/^(0\d|\d{2}):?(0\d|\d{2})$/);
@@ -40,6 +42,17 @@ const QueryOptionsCard = () => {
const [toTime, setToTime] = useState(undefined);
const [selectedServers, setSelectedServers] = useState([]);
+ const [filters, setFilters] = useState([]);
+
+ // View & filter data
+ const {data: options, loadingError} = useDataRequest(fetchFilters, []);
+ const [graphData, setGraphData] = useState(undefined);
+ useEffect(() => {
+ if (options) {
+ console.log("Graph data loaded")
+ setGraphData({playersOnline: options.viewPoints, color: '#9E9E9E'})
+ }
+ }, [options, setGraphData]);
// View state handling
const [invalidFields, setInvalidFields] = useState([]);
@@ -47,8 +60,12 @@ const QueryOptionsCard = () => {
const setAsValid = id => setInvalidFields(invalidFields.filter(invalid => id !== invalid));
const [extremes, setExtremes] = useState(undefined);
+ /*eslint-disable react-hooks/exhaustive-deps */
+ // Because: Don't update when any of the date/time fields change because that would lead to infinite loop
const updateExtremes = useCallback(() => {
if (invalidFields.length || !options) return;
+ if (!fromDate && !fromTime && !toDate && !toTime) return;
+
const newMin = parseTime(
fromDate ? fromDate : options.view.afterDate,
fromTime ? fromTime : options.view.afterTime
@@ -61,11 +78,12 @@ const QueryOptionsCard = () => {
min: newMin,
max: newMax
});
- }, [fromDate, fromTime, toDate, toTime, invalidFields]);
- useEffect(updateExtremes, [invalidFields]);
+ }, [invalidFields, options]);
+ /* eslint-enable react-hooks/exhaustive-deps */
+ useEffect(updateExtremes, [invalidFields, updateExtremes]);
const onSetExtremes = useCallback((event) => {
- if (event) {
+ if (event && event.trigger) {
const afterDate = Highcharts.dateFormat('%d/%m/%Y', event.min);
const afterTime = Highcharts.dateFormat('%H:%M', event.min);
const beforeDate = Highcharts.dateFormat('%d/%m/%Y', event.max);
@@ -77,15 +95,6 @@ const QueryOptionsCard = () => {
}
}, [setFromTime, setFromDate, setToTime, setToDate]);
- // View & filter data
- const {data: options, loadingError} = useDataRequest(fetchFilters, []);
- const [graphData, setGraphData] = useState(undefined);
- useEffect(() => {
- if (options) {
- setGraphData({playersOnline: options.viewPoints, color: '#9E9E9E'})
- }
- }, [options, setGraphData]);
-
const getServerSelectorMessage = () => {
const selected = selectedServers.length;
const available = options.view.servers.length;
@@ -176,7 +185,18 @@ const QueryOptionsCard = () => {
-
+
+
+
+
+
+
+
+
+
+
+