flag updates actually save
This commit is contained in:
parent
9f80e1cd0f
commit
2da5c82360
280
.eslintrc.json
280
.eslintrc.json
@ -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"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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);
|
||||||
|
if (response.success)
|
||||||
setUnsaved(false);
|
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> [{flag.type}] ({flag.id})
|
<b>{flag.name}</b> [{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')
|
||||||
|
return <div>
|
||||||
{elements}
|
{elements}
|
||||||
</div>;
|
</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'>
|
||||||
|
|
||||||
|
109
src/util/Util.ts
109
src/util/Util.ts
@ -6,135 +6,167 @@ type HttpOpts = {
|
|||||||
method: string
|
method: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getUser = () => {
|
export const getUser = () =>
|
||||||
|
{
|
||||||
const data = sessionStorage.getItem('user');
|
const data = sessionStorage.getItem('user');
|
||||||
if (data) return JSON.parse(data);
|
if (data)
|
||||||
|
return JSON.parse(data);
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const clearSession = () => {
|
export const clearSession = () =>
|
||||||
|
{
|
||||||
sessionStorage.removeItem('user');
|
sessionStorage.removeItem('user');
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setSession = (user: User) => {
|
export const setSession = (user: User) =>
|
||||||
|
{
|
||||||
sessionStorage.setItem('user', JSON.stringify(user));
|
sessionStorage.setItem('user', JSON.stringify(user));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchUser = async (force = false) => {
|
export const fetchUser = async (force = false) =>
|
||||||
|
{
|
||||||
const user = getUser();
|
const user = getUser();
|
||||||
if (!force && user) return user;
|
if (!force && user)
|
||||||
|
return user;
|
||||||
|
|
||||||
const result = await get('/api/user');
|
const result = await get('/api/user');
|
||||||
if (result.status === 200) {
|
if (result.status === 200)
|
||||||
|
{
|
||||||
setSession(result.data as User);
|
setSession(result.data as User);
|
||||||
return result.data;
|
return result.data;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setSettings = (settings: ClientSettings) => {
|
export const setSettings = (settings: ClientSettings) =>
|
||||||
if(settings) sessionStorage.setItem('settings', JSON.stringify(settings));
|
{
|
||||||
|
if(settings)
|
||||||
|
sessionStorage.setItem('settings', JSON.stringify(settings));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSettings = (): ClientSettings | null => {
|
export const getSettings = (): ClientSettings | null =>
|
||||||
|
{
|
||||||
const data = sessionStorage.getItem('settings');
|
const data = sessionStorage.getItem('settings');
|
||||||
if (data) return JSON.parse(data);
|
if (data)
|
||||||
|
return JSON.parse(data);
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseResponse = async (response: Response): Promise<Res> => {
|
const parseResponse = async (response: Response): Promise<Res> =>
|
||||||
|
{
|
||||||
const { headers: rawHeaders, status } = response;
|
const { headers: rawHeaders, status } = response;
|
||||||
// Fetch returns heders as an interable for some reason
|
// Fetch returns heders as an interable for some reason
|
||||||
const headers = [...rawHeaders].reduce((acc, [key, val]) => {
|
const headers = [...rawHeaders].reduce((acc, [key, val]) =>
|
||||||
|
{
|
||||||
acc[key.toLowerCase()] = val;
|
acc[key.toLowerCase()] = val;
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as {[key: string]: string});
|
}, {} as {[key: string]: string});
|
||||||
const success = status >= 200 && status < 300;
|
const success = status >= 200 && status < 300;
|
||||||
const base = { success, status };
|
const base = { success, status };
|
||||||
|
|
||||||
if (status === 401) {
|
if (status === 401)
|
||||||
|
{
|
||||||
clearSession();
|
clearSession();
|
||||||
if (!location.pathname.includes('/login')
|
if (!location.pathname.includes('/login')
|
||||||
&& !location.pathname.includes('/register')
|
&& !location.pathname.includes('/register')
|
||||||
&& location.pathname !== '/') location.pathname = '/login';
|
&& location.pathname !== '/')
|
||||||
|
location.pathname = '/login';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (headers['content-type']?.includes('application/json')) {
|
if (headers['content-type']?.includes('application/json'))
|
||||||
|
{
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return {...base, data};
|
return {...base, data};
|
||||||
}
|
}
|
||||||
return { ...base, message: (await response.text() || response.statusText) };
|
return { ...base, message: (await response.text() || response.statusText) };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const post = async (url: string, body?: object | string, opts: ReqOptions = {}) => {
|
export const post = async (url: string, body?: object | string, opts: ReqOptions = {}) =>
|
||||||
|
{
|
||||||
|
|
||||||
const options: HttpOpts & RequestOptions = {
|
const options: HttpOpts & RequestOptions = {
|
||||||
method: 'post'
|
method: 'POST'
|
||||||
};
|
};
|
||||||
|
|
||||||
if (opts.headers !== null) {
|
if (opts.headers !== null)
|
||||||
|
{
|
||||||
options.headers = {
|
options.headers = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...opts.headers || {}
|
...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)
|
||||||
else options.body = body as string;
|
options.body = JSON.stringify(body);
|
||||||
|
else
|
||||||
|
options.body = body as string;
|
||||||
|
|
||||||
console.log(url, options);
|
console.log(url, options);
|
||||||
const response = await fetch(url, options);
|
const response = await fetch(url, options);
|
||||||
return parseResponse(response);
|
return parseResponse(response);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const patch = async (url: string, body: object | string, opts: RequestOptions = {}) => {
|
export const patch = async (url: string, body: object | string, opts: RequestOptions = {}) =>
|
||||||
|
{
|
||||||
const options: HttpOpts & RequestOptions = {
|
const options: HttpOpts & RequestOptions = {
|
||||||
method: 'patch'
|
method: 'PATCH'
|
||||||
};
|
};
|
||||||
|
|
||||||
if (opts.headers !== null) {
|
if (opts.headers !== null)
|
||||||
|
{
|
||||||
options.headers = {
|
options.headers = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...opts.headers || {}
|
...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)
|
||||||
else options.body = body as string;
|
options.body = JSON.stringify(body);
|
||||||
|
else
|
||||||
|
options.body = body as string;
|
||||||
|
|
||||||
|
console.log(url, options);
|
||||||
const response = await fetch(url, options);
|
const response = await fetch(url, options);
|
||||||
return parseResponse(response);
|
return parseResponse(response);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const get = async (url: string, params?: {[key: string]: boolean | string | number | string[] | number[]}) => {
|
export const get = async (url: string, params?: {[key: string]: boolean | string | number | string[] | number[]}) =>
|
||||||
if (params) url += '?' + Object.entries(params)
|
{
|
||||||
|
if (params)
|
||||||
|
url += '?' + Object.entries(params)
|
||||||
.map(([key, val]) => `${key}=${val instanceof Array ? val.join(',') : val}`)
|
.map(([key, val]) => `${key}=${val instanceof Array ? val.join(',') : val}`)
|
||||||
.join('&');
|
.join('&');
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
return parseResponse(response);
|
return parseResponse(response);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const del = async (url: string, body?: object | string, opts: RequestOptions = {}) => {
|
export const del = async (url: string, body?: object | string, opts: RequestOptions = {}) =>
|
||||||
|
{
|
||||||
const options: HttpOpts & RequestOptions = {
|
const options: HttpOpts & RequestOptions = {
|
||||||
method: 'delete'
|
method: 'DELETE'
|
||||||
};
|
};
|
||||||
|
|
||||||
if (opts.headers !== null) {
|
if (opts.headers !== null)
|
||||||
|
{
|
||||||
options.headers = {
|
options.headers = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...opts.headers || {}
|
...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)
|
||||||
else options.body = body as string;
|
options.body = JSON.stringify(body);
|
||||||
|
else
|
||||||
|
options.body = body as string;
|
||||||
|
|
||||||
const response = await fetch(url, options);
|
const response = await fetch(url, options);
|
||||||
return parseResponse(response);
|
return parseResponse(response);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const capitalise = (str: string) => {
|
export const capitalise = (str: string) =>
|
||||||
|
{
|
||||||
const first = str[0].toUpperCase();
|
const first = str[0].toUpperCase();
|
||||||
return `${first}${str.substring(1)}`;
|
return `${first}${str.substring(1)}`;
|
||||||
};
|
};
|
||||||
@ -142,12 +174,15 @@ export const capitalise = (str: string) => {
|
|||||||
export type OrganisedFlags = {
|
export type OrganisedFlags = {
|
||||||
[key: string]: Flag | OrganisedFlags
|
[key: string]: Flag | OrganisedFlags
|
||||||
}
|
}
|
||||||
export const organiseFlags = (flags: Flag[]) => {
|
export const organiseFlags = (flags: Flag[]) =>
|
||||||
|
{
|
||||||
const obj: OrganisedFlags = {};
|
const obj: OrganisedFlags = {};
|
||||||
for (const flag of flags) {
|
for (const flag of flags)
|
||||||
|
{
|
||||||
const hierarchy = flag.hierarchy.split(':');
|
const hierarchy = flag.hierarchy.split(':');
|
||||||
let selected = obj;
|
let selected = obj;
|
||||||
for (let i = 0; i < hierarchy.length; i++) {
|
for (let i = 0; i < hierarchy.length; i++)
|
||||||
|
{
|
||||||
const rank = hierarchy[i];
|
const rank = hierarchy[i];
|
||||||
if (!selected[rank])
|
if (!selected[rank])
|
||||||
selected[rank] = {} as OrganisedFlags;
|
selected[rank] = {} as OrganisedFlags;
|
||||||
|
Loading…
Reference in New Issue
Block a user