flag updates actually save
This commit is contained in:
parent
9f80e1cd0f
commit
2da5c82360
284
.eslintrc.json
284
.eslintrc.json
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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> [{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'>
|
||||
|
||||
|
351
src/util/Util.ts
351
src/util/Util.ts
@ -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;
|
||||
};
|
Loading…
Reference in New Issue
Block a user