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 "es2021": true
}, },
"extends": [ "extends": [
"eslint:recommended", "eslint:recommended",
"plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended",
"plugin:react/recommended" "plugin:react/recommended"
], ],
@ -20,284 +20,14 @@
"react" "react"
], ],
"rules": { "rules": {
"@typescript-eslint/no-unused-vars": "off", "nonblock-statement-body-position": [
"@typescript-eslint/no-var-requires":"off", "warn",
"react/no-unescaped-entities": "warn", "below"
"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"
], ],
"brace-style": [ "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", "warn",
"property" "allman"
], ],
"dot-notation": [ "indent": "warn"
"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"
]
} }
} }

View File

@ -1,102 +1,102 @@
.input-group { .input-group {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
} }
.input-group > button { .input-group > button {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.flag-list-tiles { .flag-list-tiles {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 10px gap: 10px
} }
.flag { .flag {
/* background-color: var(--bg-color); */ /* background-color: var(--bg-color); */
} }
details .flag { details .flag {
margin: 1.7rem; margin: 1.7rem;
padding-top: 5px; padding-top: 5px;
} }
.tile { .tile {
width: calc(25% - 8px); width: calc(25% - 8px);
background-color: var(--bg-color); background-color: var(--bg-color);
} }
.flag.list-element { .flag.list-element {
margin-left: 20px; margin-left: 20px;
user-select: text; user-select: text;
padding: 1rem 1rem; padding: 1rem 1rem;
} }
.flag span.clickable:hover{ .flag span.clickable:hover{
font-style: italic; font-style: italic;
} }
.list.category { .list.category {
user-select: none; user-select: none;
background-color: var(--color-darkGrey); background-color: var(--color-darkGrey);
padding: 1rem; padding: 1rem;
margin: 0.5rem 0rem; margin: 0.5rem 0rem;
border-radius: 1rem; border-radius: 1rem;
border: 2px solid var(--bg-color); border: 2px solid var(--bg-color);
} }
.list.category > * { .list.category > * {
margin-left: 10px; margin-left: 10px;
margin-right: 10px; margin-right: 10px;
} }
.popup .flag { .popup .flag {
height: 500px; height: 500px;
width: 500px; width: 500px;
} }
.list .listBody{ .list .listBody{
display: none; display: none;
} }
.list.open > .listBody{ .list.open > .listBody{
display: inherit; display: inherit;
} }
.list .listHeader{ .list .listHeader{
display: flex; display: flex;
align-items: center; align-items: center;
} }
.list-carrot{ .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"); 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; width: 10px;
height: 10px; height: 10px;
transition: transform ease-in-out 0.1s; transition: transform ease-in-out 0.1s;
background-size: 10px; background-size: 10px;
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: center; background-position: center;
transform: rotate(-180deg); transform: rotate(-180deg);
display: inline-block; display: inline-block;
margin-right: 8px; margin-right: 8px;
margin-left: 3px; margin-left: 3px;
} }
.list-element:has(div.unsaved) { .list-element:has(div.unsaved) {
outline: solid 1px #ffb300a6; outline: solid 1px #ffb300a6;
} }
.list.open > .listHeader > .list-carrot, .list .listBody details[open] summary > .list-carrot{ .list.open > .listHeader > .list-carrot, .list .listBody details[open] summary > .list-carrot{
transform: rotate(-90deg); transform: rotate(-90deg);
transition: transform ease-in-out 0.1s; transition: transform ease-in-out 0.1s;
} }
.list .listBody details summary::marker{ .list .listBody details summary::marker{
content: ""; content: "";
} }
.list .listBody details summary{ .list .listBody details summary{
display: flex; display: flex;
align-items: center; align-items: center;
} }
.unsaved-changes { .unsaved-changes {
outline: solid 1px #ffb300a6; outline: solid 1px #ffb300a6;
} }
.list input{ .list input{
box-sizing: border-box; box-sizing: border-box;
} }

View File

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

View File

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