flag updates actually save

This commit is contained in:
Erik 2023-07-05 13:41:40 +03:00
parent 9f80e1cd0f
commit 2da5c82360
Signed by: Navy.gif
GPG Key ID: 2532FBBB61C65A68
4 changed files with 419 additions and 613 deletions

View File

@ -5,7 +5,7 @@
"es2021": true
},
"extends": [
"eslint:recommended",
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended"
],
@ -20,284 +20,14 @@
"react"
],
"rules": {
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-var-requires":"off",
"react/no-unescaped-entities": "warn",
"react/prop-types": "off",
"accessor-pairs": "error",
"array-bracket-newline": "error",
"array-bracket-spacing": [
"error",
"never"
],
"array-callback-return": "error",
"array-element-newline": "off",
"arrow-body-style": "off",
"arrow-parens": "off",
"arrow-spacing": [
"error",
{
"after": true,
"before": true
}
],
"block-scoped-var": "error",
"block-spacing": [
"error",
"always"
"nonblock-statement-body-position": [
"warn",
"below"
],
"brace-style": [
"error",
"1tbs",
{
"allowSingleLine": true
}
],
"camelcase": "error",
"capitalized-comments": "off",
"class-methods-use-this": "error",
"comma-dangle": "error",
"comma-spacing": "off",
"comma-style": [
"error",
"last"
],
"complexity": "off",
"computed-property-spacing": [
"error",
"never"
],
"consistent-return": "off",
"consistent-this": "error",
"curly": "off",
"default-case": "error",
"default-case-last": "error",
"default-param-last": "error",
"dot-location": [
"warn",
"property"
"allman"
],
"dot-notation": [
"error",
{
"allowKeywords": true
}
],
"eol-last": "off",
"eqeqeq": "error",
"func-call-spacing": "error",
"func-name-matching": "error",
"func-names": "error",
"func-style": [
"error",
"declaration",
{
"allowArrowFunctions": true
}
],
"function-call-argument-newline": [
"error",
"consistent"
],
"function-paren-newline": "error",
"generator-star-spacing": "error",
"grouped-accessor-pairs": "error",
"guard-for-in": "error",
"id-denylist": "error",
"id-length": "off",
"id-match": "error",
"implicit-arrow-linebreak": [
"error",
"beside"
],
"indent": "off",
"init-declarations": "error",
"jsx-quotes": "off",
"key-spacing": "off",
"keyword-spacing": "off",
"line-comment-position": "off",
"linebreak-style": "off",
"lines-around-comment": "error",
"max-classes-per-file": "error",
"max-depth": "error",
"max-len": "off",
// "max-lines": "error",
"max-lines-per-function": "off",
"max-nested-callbacks": "error",
"max-params": "off",
"max-statements": "off",
"max-statements-per-line": "error",
"multiline-comment-style": [
"error",
"separate-lines"
],
"multiline-ternary": [
"error",
"always-multiline"
],
"new-cap": "error",
"new-parens": "error",
"newline-per-chained-call": "error",
"no-alert": "error",
"no-array-constructor": "error",
"no-await-in-loop": "error",
"no-bitwise": "error",
"no-caller": "error",
"no-confusing-arrow": "error",
"no-console": "off",
"no-constructor-return": "error",
"no-continue": "error",
"no-div-regex": "error",
"no-duplicate-imports": "error",
"no-else-return": [
"error",
{
"allowElseIf": true
}
],
"no-empty-function": "off",
"no-eq-null": "error",
"no-eval": "error",
"no-extend-native": "error",
"no-extra-bind": "error",
"no-extra-label": "error",
// "no-extra-parens": "error",
"no-floating-decimal": "error",
"no-implicit-coercion": "error",
"no-implicit-globals": "error",
"no-implied-eval": "error",
"no-inline-comments": "off",
"no-invalid-this": "error",
"no-iterator": "error",
"no-label-var": "error",
"no-labels": "error",
"no-lone-blocks": "error",
"no-lonely-if": "error",
"no-loop-func": "error",
"no-loss-of-precision": "error",
"no-magic-numbers": "off",
"no-mixed-operators": "error",
"no-multi-assign": "error",
"no-multi-spaces": "error",
"no-multi-str": "error",
"no-multiple-empty-lines": "error",
"no-negated-condition": "off",
"no-nested-ternary": "error",
"no-new": "error",
"no-new-func": "error",
"no-new-object": "error",
"no-new-wrappers": "error",
"no-nonoctal-decimal-escape": "error",
"no-octal-escape": "error",
"no-param-reassign": "off",
"no-plusplus": "off",
"no-promise-executor-return": "error",
"no-proto": "error",
"no-restricted-exports": "error",
"no-restricted-globals": "error",
"no-restricted-imports": "error",
"no-restricted-properties": "error",
"no-restricted-syntax": "error",
"no-return-assign": "error",
"no-return-await": "error",
"no-script-url": "error",
"no-self-compare": "error",
"no-sequences": "error",
"no-shadow": "off",
"no-tabs": "error",
"no-template-curly-in-string": "error",
"no-ternary": "off",
"no-throw-literal": "error",
"no-trailing-spaces": "off",
"no-undef-init": "error",
"no-undefined": "error",
"no-underscore-dangle": "off",
"no-unmodified-loop-condition": "error",
"no-unneeded-ternary": "error",
"no-unreachable-loop": "error",
"no-unsafe-optional-chaining": "error",
"no-unused-expressions": "error",
"no-use-before-define": "off",
"no-useless-backreference": "error",
"no-useless-call": "error",
"no-useless-computed-key": "error",
"no-useless-concat": "error",
"no-useless-constructor": "error",
"no-useless-rename": "error",
"no-useless-return": "error",
"no-var": "error",
"no-void": "error",
"no-warning-comments": "warn",
"no-whitespace-before-property": "error",
// "nonblock-statement-body-position": ["warn", "below"],
"object-curly-newline": "error",
"object-curly-spacing": "off",
// "object-property-newline": "error",
"object-shorthand": "error",
"one-var": "off",
"one-var-declaration-per-line": "error",
"operator-assignment": [
"error",
"always"
],
"operator-linebreak": "off",
"padded-blocks": "off",
"padding-line-between-statements": "error",
"prefer-arrow-callback": "error",
"prefer-const": "error",
"prefer-destructuring": "error",
"prefer-exponentiation-operator": "error",
"prefer-named-capture-group": "error",
"prefer-numeric-literals": "error",
"prefer-object-spread": "error",
"prefer-promise-reject-errors": "error",
"prefer-regex-literals": "error",
"prefer-rest-params": "error",
"prefer-spread": "error",
"prefer-template": "off",
"quote-props": "off",
"quotes": "off",
"radix": [
"error",
"as-needed"
],
"require-atomic-updates": "error",
"require-await": "error",
"require-unicode-regexp": "off",
"rest-spread-spacing": "error",
"semi": "warn",
"semi-spacing": "warn",
"semi-style": [
"error",
"last"
],
"sort-keys": "off",
"sort-vars": "off",
"space-before-blocks": "error",
"space-before-function-paren": "off",
"space-in-parens": [
"error",
"never"
],
"space-infix-ops": "off",
"space-unary-ops": "error",
"spaced-comment": "off",
"strict": "error",
"switch-colon-spacing": "error",
"symbol-description": "error",
"template-curly-spacing": "off",
"template-tag-spacing": "error",
"unicode-bom": [
"error",
"never"
],
"vars-on-top": "error",
"wrap-iife": "error",
"wrap-regex": "error",
"yield-star-spacing": "error",
"yoda": [
"error",
"never"
]
"indent": "warn"
}
}
}

View File

@ -1,102 +1,102 @@
.input-group {
display: flex;
flex-direction: row;
}
.input-group > button {
margin-bottom: 1rem;
}
.flag-list-tiles {
display: flex;
flex-wrap: wrap;
gap: 10px
}
.flag {
/* background-color: var(--bg-color); */
}
details .flag {
margin: 1.7rem;
padding-top: 5px;
}
.tile {
width: calc(25% - 8px);
background-color: var(--bg-color);
}
.flag.list-element {
margin-left: 20px;
user-select: text;
padding: 1rem 1rem;
}
.flag span.clickable:hover{
font-style: italic;
}
.list.category {
user-select: none;
background-color: var(--color-darkGrey);
padding: 1rem;
margin: 0.5rem 0rem;
border-radius: 1rem;
border: 2px solid var(--bg-color);
}
.list.category > * {
margin-left: 10px;
margin-right: 10px;
}
.popup .flag {
height: 500px;
width: 500px;
}
.list .listBody{
display: none;
}
.list.open > .listBody{
display: inherit;
}
.list .listHeader{
display: flex;
align-items: center;
}
.list-carrot{
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' version='1.1' id='Layer_1' x='0px' y='0px' viewBox='0 0 512 512' style='enable-background:new 0 0 512 512;' xml:space='preserve'%3E%3Cg%3E%3Cg%3E%3Cpath fill='%23f5f5f5' d='M441.751,475.584L222.166,256L441.75,36.416c6.101-6.101,7.936-15.275,4.629-23.253C443.094,5.184,435.286,0,426.667,0 H320.001c-5.675,0-11.093,2.24-15.083,6.251L70.251,240.917c-8.341,8.341-8.341,21.824,0,30.165l234.667,234.667 c3.989,4.011,9.408,6.251,15.083,6.251h106.667c8.619,0,16.427-5.184,19.712-13.163 C449.687,490.858,447.852,481.685,441.751,475.584z'/%3E%3C/g%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3C/svg%3E");
width: 10px;
height: 10px;
transition: transform ease-in-out 0.1s;
background-size: 10px;
background-repeat: no-repeat;
background-position: center;
transform: rotate(-180deg);
display: inline-block;
margin-right: 8px;
margin-left: 3px;
}
.list-element:has(div.unsaved) {
outline: solid 1px #ffb300a6;
}
.list.open > .listHeader > .list-carrot, .list .listBody details[open] summary > .list-carrot{
transform: rotate(-90deg);
transition: transform ease-in-out 0.1s;
}
.list .listBody details summary::marker{
content: "";
}
.list .listBody details summary{
display: flex;
align-items: center;
}
.unsaved-changes {
outline: solid 1px #ffb300a6;
}
.list input{
box-sizing: border-box;
.input-group {
display: flex;
flex-direction: row;
}
.input-group > button {
margin-bottom: 1rem;
}
.flag-list-tiles {
display: flex;
flex-wrap: wrap;
gap: 10px
}
.flag {
/* background-color: var(--bg-color); */
}
details .flag {
margin: 1.7rem;
padding-top: 5px;
}
.tile {
width: calc(25% - 8px);
background-color: var(--bg-color);
}
.flag.list-element {
margin-left: 20px;
user-select: text;
padding: 1rem 1rem;
}
.flag span.clickable:hover{
font-style: italic;
}
.list.category {
user-select: none;
background-color: var(--color-darkGrey);
padding: 1rem;
margin: 0.5rem 0rem;
border-radius: 1rem;
border: 2px solid var(--bg-color);
}
.list.category > * {
margin-left: 10px;
margin-right: 10px;
}
.popup .flag {
height: 500px;
width: 500px;
}
.list .listBody{
display: none;
}
.list.open > .listBody{
display: inherit;
}
.list .listHeader{
display: flex;
align-items: center;
}
.list-carrot{
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' version='1.1' id='Layer_1' x='0px' y='0px' viewBox='0 0 512 512' style='enable-background:new 0 0 512 512;' xml:space='preserve'%3E%3Cg%3E%3Cg%3E%3Cpath fill='%23f5f5f5' d='M441.751,475.584L222.166,256L441.75,36.416c6.101-6.101,7.936-15.275,4.629-23.253C443.094,5.184,435.286,0,426.667,0 H320.001c-5.675,0-11.093,2.24-15.083,6.251L70.251,240.917c-8.341,8.341-8.341,21.824,0,30.165l234.667,234.667 c3.989,4.011,9.408,6.251,15.083,6.251h106.667c8.619,0,16.427-5.184,19.712-13.163 C449.687,490.858,447.852,481.685,441.751,475.584z'/%3E%3C/g%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3C/svg%3E");
width: 10px;
height: 10px;
transition: transform ease-in-out 0.1s;
background-size: 10px;
background-repeat: no-repeat;
background-position: center;
transform: rotate(-180deg);
display: inline-block;
margin-right: 8px;
margin-left: 3px;
}
.list-element:has(div.unsaved) {
outline: solid 1px #ffb300a6;
}
.list.open > .listHeader > .list-carrot, .list .listBody details[open] summary > .list-carrot{
transform: rotate(-90deg);
transition: transform ease-in-out 0.1s;
}
.list .listBody details summary::marker{
content: "";
}
.list .listBody details summary{
display: flex;
align-items: center;
}
.unsaved-changes {
outline: solid 1px #ffb300a6;
}
.list input{
box-sizing: border-box;
}

View File

@ -1,108 +1,132 @@
import React, { useEffect, useRef, useState } from "react";
import { Flag as APIFlag } from "../../@types/ApiStructures";
import { OrganisedFlags, capitalise, get, organiseFlags } from "../../util/Util";
import { ClickToEdit, Dropdown, InputElementType, NumberInput, StringInput, ToggleSwitch } from "../../components/InputElements";
import { PageButtons } from "../../components/PageControls";
import React, { useEffect, useRef, useState } from 'react';
import { Flag as APIFlag } from '../../@types/ApiStructures';
import { OrganisedFlags, capitalise, get, organiseFlags, patch } from '../../util/Util';
import { ClickToEdit, Dropdown, NumberInput, StringInput, ToggleSwitch } from '../../components/InputElements';
import { PageButtons } from '../../components/PageControls';
const Flag = ({ flag: incoming }: { flag: APIFlag }) => {
const Flag = ({ flag: incoming }: { flag: APIFlag }) =>
{
const [flag, setFlag] = useState(incoming);
const [ flag, setFlag ] = useState(incoming);
const [unsaved, setUnsaved] = useState(false);
const [error, setError] = useState<string>();
const valueRef = useRef<HTMLInputElement>(null);
const updateFlag = (f: APIFlag) => {
console.log(f);
const updateFlag = (f: APIFlag) =>
{
setFlag(f);
setUnsaved(true);
};
const save = () => {
console.log('save');
console.log(flag);
setUnsaved(false);
const save = async () =>
{
const response = await patch(`/api/flags/${flag.id}`, flag);
if (response.success)
setUnsaved(false);
else
setError(response.message)
};
let Input = <p>Loading...</p>;
if (flag.type === 'string') Input = <StringInput onChange={() => setUnsaved(true)} inputRef={valueRef} value={flag.value as string} />;
else if (flag.type === 'number') Input = <NumberInput onChange={() => setUnsaved(true)} inputRef={valueRef} value={flag.value as number} type='float' />;
else if (flag.type === 'boolean') Input = <ToggleSwitch onChange={() => setUnsaved(true)} inputRef={valueRef} value={flag.value as boolean} />;
if (flag.type === 'string')
Input = <StringInput onChange={() => setUnsaved(true)} inputRef={valueRef} value={flag.value as string} />;
else if (flag.type === 'number')
Input = <NumberInput onChange={() => setUnsaved(true)} inputRef={valueRef} value={flag.value as number} type='float' />;
else if (flag.type === 'boolean')
Input = <ToggleSwitch onChange={() => setUnsaved(true)} inputRef={valueRef} value={flag.value as boolean} />;
return <div className='flag mt-0 mb-1'>
{/* TODO: Improve these*/}
{unsaved && <i><small>Unsaved changes</small></i>}
<h3 className="mt-0 mb-1">
{error && <p>{error}</p>}
<h3 className='mt-0 mb-1'>
<ClickToEdit onUpdate={val => updateFlag({ ...flag, name: val })} value={flag.name} />
</h3>
<p className="mt-0 mb-1"><b>ID:</b> {flag.id}</p>
<p className="mt-0 mb-1">
<p className='mt-0 mb-1'><b>ID:</b> {flag.id}</p>
<p className='mt-0 mb-1'>
<b>Environment: </b>
<ClickToEdit onUpdate={val => updateFlag({ ...flag, env: val })} value={flag.env} />
</p>
<p className="mt-0 mb-1">
<p className='mt-0 mb-1'>
<b>Consumer: </b>
<ClickToEdit onUpdate={val => updateFlag({ ...flag, consumer: val })} value={flag.consumer} />
</p>
<p className="mt-0 mb-1">
<p className='mt-0 mb-1'>
<b>Hierarchy: </b>
<ClickToEdit onUpdate={val => updateFlag({ ...flag, hierarchy: val })} value={flag.hierarchy} />
</p>
<p className="mt-0 mb-1">
<p className='mt-0 mb-1'>
<b>Value: </b>
{Input}
</p>
<button className="button danger">Delete</button>
{unsaved && <button onClick={save} className="button primary">Save</button>}
<button className='button danger'>Delete</button>
{unsaved && <button onClick={save} className='button primary'>Save</button>}
</div>;
};
const FlagTile = ({ flag }: { flag: APIFlag }) => {
return <div className="card tile">
const FlagTile = ({ flag }: { flag: APIFlag }) =>
{
return <div className='card tile'>
<Flag flag={flag} />
</div>;
};
const FlagListElement = ({ flag }: { flag: APIFlag, onClick?: React.ReactEventHandler }) => {
const FlagListElement = ({ flag }: { flag: APIFlag, onClick?: React.ReactEventHandler }) =>
{
return <details className='card list-element mt-2 mb-2'>
<summary className="clickable">
<i className="list-carrot"></i>
<summary className='clickable'>
<i className='list-carrot'></i>
<b>{flag.name}</b>&nbsp;[{flag.type}] ({flag.id})
</summary>
<Flag flag={flag} />
</details>;
};
const ListCategory = ({ flags, name, setFlag }: { flags: OrganisedFlags, name: string, setFlag?: (flag: APIFlag) => void }) => {
const ListCategory = ({ flags, name, setFlag }: { flags: OrganisedFlags, name: string, setFlag?: (flag: APIFlag) => void }) =>
{
const categories = Object.keys(flags);
const elements: JSX.Element[] = [];
for (const category of categories) {
for (const category of categories)
{
const element = flags[category];
if (element.id)
elements.push(<FlagListElement onClick={() => setFlag && setFlag(element as APIFlag)} key={element.id as string} flag={element as APIFlag} />);
elements.push(<FlagListElement
onClick={() => setFlag && setFlag(element as APIFlag)}
key={element.id as string}
flag={element as APIFlag}
/>);
else
elements.push(<ListCategory setFlag={setFlag} key={category} name={category} flags={element as OrganisedFlags} />);
}
const [hidden, setHidden] = useState(true);
const [ hidden, setHidden ] = useState(true);
if (name === 'root') return <div>
{elements}
</div>;
if (name === 'root')
return <div>
{elements}
</div>;
return <div className={`list category${ hidden ? "" : " open" }`}>
<b className="clickable listHeader" onClick={() => { setHidden(!hidden); }}><i className="list-carrot"></i> {capitalise(name || 'Unordered')}</b>
<div className="listBody">
return <div className={`list category${hidden ? '' : ' open'}`}>
<b className='clickable listHeader' onClick={() =>
{
setHidden(!hidden);
}}><i className='list-carrot'></i> {capitalise(name || 'Unordered')}</b>
<div className='listBody'>
{elements}
</div>
</div>;
};
const ListView = ({ flags }: { flags: APIFlag[] }) => {
const ListView = ({ flags }: { flags: APIFlag[] }) =>
{
const organised = organiseFlags(flags);
const [currentFlag, setCurrentFlag] = useState<APIFlag | null>(null);
const selectFlag = (flag: APIFlag) => {
const [ currentFlag, setCurrentFlag ] = useState<APIFlag | null>(null);
const selectFlag = (flag: APIFlag) =>
{
console.log(flag);
setCurrentFlag(flag);
};
@ -119,9 +143,10 @@ const ListView = ({ flags }: { flags: APIFlag[] }) => {
return <ListCategory setFlag={(flag: APIFlag) => selectFlag(flag)} name={'root'} flags={organised} />;
};
const TileView = ({ flags, children }: { flags: APIFlag[], children: React.ReactNode }) => {
const TileView = ({ flags, children }: { flags: APIFlag[], children: React.ReactNode }) =>
{
return <div>
<div className="flag-list-tiles">
<div className='flag-list-tiles'>
{flags.map(flag => <FlagTile key={flag.id} flag={flag} />)}
</div>
{children}
@ -129,34 +154,43 @@ const TileView = ({ flags, children }: { flags: APIFlag[], children: React.React
};
const FlagList = () => {
const FlagList = () =>
{
const searchBarRef = useRef<HTMLInputElement>(null);
const [flags, setFlags] = useState<APIFlag[]>([]);
const [error, setError] = useState<string | null>(null);
const [selectedFilters, setSelectedFilters] = useState<string[]>([]);
const [availableFilters, setAvailableFilters] = useState<string[] | null>(null);
const [nameSearch, setNameSearch] = useState<string>('');
const [page, setPage] = useState(1);
const [pages, setPages] = useState(1);
const [listView, toggleList] = useState(false);
const [ flags, setFlags ] = useState<APIFlag[]>([]);
const [ error, setError ] = useState<string | null>(null);
const [ selectedFilters, setSelectedFilters ] = useState<string[]>([]);
const [ availableFilters, setAvailableFilters ] = useState<string[] | null>(null);
const [ nameSearch, setNameSearch ] = useState<string>('');
const [ page, setPage ] = useState(1);
const [ pages, setPages ] = useState(1);
const [ listView, toggleList ] = useState(false);
useEffect(() => {
(async () => {
useEffect(() =>
{
(async () =>
{
const query: { [key: string]: string[] | boolean } = {};
for (const selected of selectedFilters) {
const [prop, val] = selected.split(':');
if (!query[prop]) query[prop] = [val];
else (query[prop] as string[]).push(val);
for (const selected of selectedFilters)
{
const [ prop, val ] = selected.split(':');
if (query[prop])
(query[prop] as string[]).push(val);
else
query[prop] = [ val ];
}
if (nameSearch) query.name = [nameSearch];
if (listView) query.all = true;
if (nameSearch)
query.name = [ nameSearch ];
if (listView)
query.all = true;
const response = await get('/api/flags', query);
if (!response.success || !response.data) {
if (!response.success || !response.data)
{
setFlags([]);
return setError(response.message as string);
}
@ -168,7 +202,7 @@ const FlagList = () => {
setError(null);
})();
}, [selectedFilters, nameSearch, page, listView]);
}, [ selectedFilters, nameSearch, page, listView ]);
return <div className='col-12-lg col-12'>
@ -176,24 +210,27 @@ const FlagList = () => {
<div>
<StringInput
inputRef={searchBarRef}
onBlur={() => {
onBlur={() =>
{
setNameSearch(searchBarRef.current?.value || '');
}}
onKeyUp={(event) => {
onKeyUp={(event) =>
{
if (event.key === 'Enter')
setNameSearch(searchBarRef.current?.value || '');
}}
placeholder="Flag name or ID"
placeholder='Flag name or ID'
/>
{availableFilters && <Dropdown>
<Dropdown.Header>
{selectedFilters.length === 0
? <p className="placeholder">Click to filter</p>
? <p className='placeholder'>Click to filter</p>
: selectedFilters.map(f => <Dropdown.Item
key={f}
onClick={() => {
onClick={() =>
{
setSelectedFilters(selectedFilters.filter(ff => f !== ff));
setAvailableFilters([...availableFilters, f]);
setAvailableFilters([ ...availableFilters, f ]);
}}
>
{f}
@ -204,9 +241,10 @@ const FlagList = () => {
<Dropdown.ItemList>
{availableFilters.map(f => <Dropdown.Item
key={f}
onClick={() => {
onClick={() =>
{
setAvailableFilters(availableFilters.filter(ff => f !== ff));
setSelectedFilters([...selectedFilters, f]);
setSelectedFilters([ ...selectedFilters, f ]);
}}
>
{f}
@ -216,13 +254,14 @@ const FlagList = () => {
</div>
<h4>Flags</h4>
<ToggleSwitch value={listView} onChange={(event) => {
<ToggleSwitch value={listView} onChange={(event) =>
{
toggleList(event.target.checked);
}}>List View:</ToggleSwitch>
{error && <p>{error}</p>}
{listView ?
<ListView flags={flags} /> :
<TileView flags={flags}>
{listView
? <ListView flags={flags} />
: <TileView flags={flags}>
<PageButtons {...{ page, setPage, pages }} />
</TileView>}
@ -231,7 +270,8 @@ const FlagList = () => {
};
const CreateFlag = () => {
const CreateFlag = () =>
{
return <div className='col-6-lg col-12'>
<h4>Create Flag</h4>
@ -241,7 +281,8 @@ const CreateFlag = () => {
</div>;
};
const Flags = () => {
const Flags = () =>
{
return <div className='row'>

View File

@ -1,159 +1,194 @@
import { Flag, User } from "../@types/ApiStructures";
import { RequestOptions, ClientSettings, Res, ReqOptions } from "../@types/Other";
type HttpOpts = {
body?: string,
method: string
}
export const getUser = () => {
const data = sessionStorage.getItem('user');
if (data) return JSON.parse(data);
return null;
};
export const clearSession = () => {
sessionStorage.removeItem('user');
};
export const setSession = (user: User) => {
sessionStorage.setItem('user', JSON.stringify(user));
};
export const fetchUser = async (force = false) => {
const user = getUser();
if (!force && user) return user;
const result = await get('/api/user');
if (result.status === 200) {
setSession(result.data as User);
return result.data;
}
return result;
};
export const setSettings = (settings: ClientSettings) => {
if(settings) sessionStorage.setItem('settings', JSON.stringify(settings));
};
export const getSettings = (): ClientSettings | null => {
const data = sessionStorage.getItem('settings');
if (data) return JSON.parse(data);
return null;
};
const parseResponse = async (response: Response): Promise<Res> => {
const { headers: rawHeaders, status } = response;
// Fetch returns heders as an interable for some reason
const headers = [...rawHeaders].reduce((acc, [key, val]) => {
acc[key.toLowerCase()] = val;
return acc;
}, {} as {[key: string]: string});
const success = status >= 200 && status < 300;
const base = { success, status };
if (status === 401) {
clearSession();
if (!location.pathname.includes('/login')
&& !location.pathname.includes('/register')
&& location.pathname !== '/') location.pathname = '/login';
}
if (headers['content-type']?.includes('application/json')) {
const data = await response.json();
return {...base, data};
}
return { ...base, message: (await response.text() || response.statusText) };
};
export const post = async (url: string, body?: object | string, opts: ReqOptions = {}) => {
const options: HttpOpts & RequestOptions = {
method: 'post'
};
if (opts.headers !== null) {
options.headers = {
'Content-Type': 'application/json',
...opts.headers || {}
};
}
if (options.headers && options.headers['Content-Type'] === 'application/json' && body) options.body = JSON.stringify(body);
else options.body = body as string;
console.log(url, options);
const response = await fetch(url, options);
return parseResponse(response);
};
export const patch = async (url: string, body: object | string, opts: RequestOptions = {}) => {
const options: HttpOpts & RequestOptions = {
method: 'patch'
};
if (opts.headers !== null) {
options.headers = {
'Content-Type': 'application/json',
...opts.headers || {}
};
}
if (options.headers && options.headers['Content-Type'] === 'application/json' && body) options.body = JSON.stringify(body);
else options.body = body as string;
const response = await fetch(url, options);
return parseResponse(response);
};
export const get = async (url: string, params?: {[key: string]: boolean | string | number | string[] | number[]}) => {
if (params) url += '?' + Object.entries(params)
.map(([key, val]) => `${key}=${val instanceof Array ? val.join(',') : val}`)
.join('&');
const response = await fetch(url);
return parseResponse(response);
};
export const del = async (url: string, body?: object | string, opts: RequestOptions = {}) => {
const options: HttpOpts & RequestOptions = {
method: 'delete'
};
if (opts.headers !== null) {
options.headers = {
'Content-Type': 'application/json',
...opts.headers || {}
};
}
if (options.headers && options.headers['Content-Type'] === 'application/json' && body) options.body = JSON.stringify(body);
else options.body = body as string;
const response = await fetch(url, options);
return parseResponse(response);
};
export const capitalise = (str: string) => {
const first = str[0].toUpperCase();
return `${first}${str.substring(1)}`;
};
export type OrganisedFlags = {
[key: string]: Flag | OrganisedFlags
}
export const organiseFlags = (flags: Flag[]) => {
const obj: OrganisedFlags = {};
for (const flag of flags) {
const hierarchy = flag.hierarchy.split(':');
let selected = obj;
for (let i = 0; i < hierarchy.length; i++) {
const rank = hierarchy[i];
if (!selected[rank])
selected[rank] = {} as OrganisedFlags;
selected = selected[rank] as OrganisedFlags;
}
selected[flag.name] = flag;
}
return obj;
import { Flag, User } from "../@types/ApiStructures";
import { RequestOptions, ClientSettings, Res, ReqOptions } from "../@types/Other";
type HttpOpts = {
body?: string,
method: string
}
export const getUser = () =>
{
const data = sessionStorage.getItem('user');
if (data)
return JSON.parse(data);
return null;
};
export const clearSession = () =>
{
sessionStorage.removeItem('user');
};
export const setSession = (user: User) =>
{
sessionStorage.setItem('user', JSON.stringify(user));
};
export const fetchUser = async (force = false) =>
{
const user = getUser();
if (!force && user)
return user;
const result = await get('/api/user');
if (result.status === 200)
{
setSession(result.data as User);
return result.data;
}
return result;
};
export const setSettings = (settings: ClientSettings) =>
{
if(settings)
sessionStorage.setItem('settings', JSON.stringify(settings));
};
export const getSettings = (): ClientSettings | null =>
{
const data = sessionStorage.getItem('settings');
if (data)
return JSON.parse(data);
return null;
};
const parseResponse = async (response: Response): Promise<Res> =>
{
const { headers: rawHeaders, status } = response;
// Fetch returns heders as an interable for some reason
const headers = [...rawHeaders].reduce((acc, [key, val]) =>
{
acc[key.toLowerCase()] = val;
return acc;
}, {} as {[key: string]: string});
const success = status >= 200 && status < 300;
const base = { success, status };
if (status === 401)
{
clearSession();
if (!location.pathname.includes('/login')
&& !location.pathname.includes('/register')
&& location.pathname !== '/')
location.pathname = '/login';
}
if (headers['content-type']?.includes('application/json'))
{
const data = await response.json();
return {...base, data};
}
return { ...base, message: (await response.text() || response.statusText) };
};
export const post = async (url: string, body?: object | string, opts: ReqOptions = {}) =>
{
const options: HttpOpts & RequestOptions = {
method: 'POST'
};
if (opts.headers !== null)
{
options.headers = {
'Content-Type': 'application/json',
...opts.headers || {}
};
}
if (options.headers && options.headers['Content-Type'] === 'application/json' && body)
options.body = JSON.stringify(body);
else
options.body = body as string;
console.log(url, options);
const response = await fetch(url, options);
return parseResponse(response);
};
export const patch = async (url: string, body: object | string, opts: RequestOptions = {}) =>
{
const options: HttpOpts & RequestOptions = {
method: 'PATCH'
};
if (opts.headers !== null)
{
options.headers = {
'Content-Type': 'application/json',
...opts.headers || {}
};
}
if (options.headers && options.headers['Content-Type'] === 'application/json' && body)
options.body = JSON.stringify(body);
else
options.body = body as string;
console.log(url, options);
const response = await fetch(url, options);
return parseResponse(response);
};
export const get = async (url: string, params?: {[key: string]: boolean | string | number | string[] | number[]}) =>
{
if (params)
url += '?' + Object.entries(params)
.map(([key, val]) => `${key}=${val instanceof Array ? val.join(',') : val}`)
.join('&');
const response = await fetch(url);
return parseResponse(response);
};
export const del = async (url: string, body?: object | string, opts: RequestOptions = {}) =>
{
const options: HttpOpts & RequestOptions = {
method: 'DELETE'
};
if (opts.headers !== null)
{
options.headers = {
'Content-Type': 'application/json',
...opts.headers || {}
};
}
if (options.headers && options.headers['Content-Type'] === 'application/json' && body)
options.body = JSON.stringify(body);
else
options.body = body as string;
const response = await fetch(url, options);
return parseResponse(response);
};
export const capitalise = (str: string) =>
{
const first = str[0].toUpperCase();
return `${first}${str.substring(1)}`;
};
export type OrganisedFlags = {
[key: string]: Flag | OrganisedFlags
}
export const organiseFlags = (flags: Flag[]) =>
{
const obj: OrganisedFlags = {};
for (const flag of flags)
{
const hierarchy = flag.hierarchy.split(':');
let selected = obj;
for (let i = 0; i < hierarchy.length; i++)
{
const rank = hierarchy[i];
if (!selected[rank])
selected[rank] = {} as OrganisedFlags;
selected = selected[rank] as OrganisedFlags;
}
selected[flag.name] = flag;
}
return obj;
};