cleanup: refactor api architecture & add user roles

This commit is contained in:
Jordan Knott
2020-07-04 18:02:57 -05:00
parent a3958595cd
commit eaffaa70df
141 changed files with 12487 additions and 3792 deletions

View File

@ -0,0 +1,501 @@
{
"header": {
"reportVersion": 1,
"event": "Allocation failed - JavaScript heap out of memory",
"trigger": "FatalError",
"filename": "report.20200630.191315.2921.0.001.json",
"dumpEventTime": "2020-06-30T19:13:15Z",
"dumpEventTimeStamp": "1593562395582",
"processId": 2921,
"cwd": "/home/jordan/Projects/project-citadel/web",
"commandLine": [
"/usr/bin/node",
"/home/jordan/.config/coc/extensions/node_modules/coc-tsserver/bin/tsserverForkStart",
"/home/jordan/Projects/project-citadel/web/node_modules/typescript/lib/tsserver.js",
"--allowLocalPluginLoads",
"--useInferredProjectPerProjectRoot",
"--cancellationPipeName",
"/tmp/coc-nvim-tscancellation-de3655a46bee8a6c5461.sock*",
"--npmLocation",
"\"/usr/bin/npm\"",
"--noGetErrOnBackgroundUpdate",
"--validateDefaultNpmLocation"
],
"nodejsVersion": "v13.8.0",
"glibcVersionRuntime": "2.30",
"glibcVersionCompiler": "2.30",
"wordSize": 64,
"arch": "x64",
"platform": "linux",
"componentVersions": {
"node": "13.8.0",
"v8": "7.9.317.25-node.28",
"uv": "1.34.1",
"zlib": "1.2.11",
"brotli": "1.0.7",
"ares": "1.15.0",
"modules": "79",
"nghttp2": "1.39.2",
"napi": "5",
"llhttp": "2.0.4",
"openssl": "1.1.1d",
"cldr": "36.0",
"icu": "65.1",
"tz": "2019c",
"unicode": "12.1"
},
"release": {
"name": "node",
"headersUrl": "https://nodejs.org/download/release/v13.8.0/node-v13.8.0-headers.tar.gz",
"sourceUrl": "https://nodejs.org/download/release/v13.8.0/node-v13.8.0.tar.gz"
},
"osName": "Linux",
"osRelease": "4.19.101-1-lts",
"osVersion": "#1 SMP Sat, 01 Feb 2020 16:35:36 +0000",
"osMachine": "x86_64",
"cpus": [
{
"model": "Intel(R) Core(TM) i7-6500U CPU @ 2.50GHz",
"speed": 928,
"user": 17517600,
"nice": 5000,
"sys": 2658800,
"idle": 54442200,
"irq": 146200
},
{
"model": "Intel(R) Core(TM) i7-6500U CPU @ 2.50GHz",
"speed": 1192,
"user": 16529500,
"nice": 3900,
"sys": 2713400,
"idle": 55020300,
"irq": 289200
},
{
"model": "Intel(R) Core(TM) i7-6500U CPU @ 2.50GHz",
"speed": 1197,
"user": 16764400,
"nice": 3600,
"sys": 2584400,
"idle": 55071500,
"irq": 157600
},
{
"model": "Intel(R) Core(TM) i7-6500U CPU @ 2.50GHz",
"speed": 1196,
"user": 16700100,
"nice": 2500,
"sys": 2588500,
"idle": 55230400,
"irq": 152300
}
],
"networkInterfaces": [
{
"name": "lo",
"internal": true,
"mac": "00:00:00:00:00:00",
"address": "127.0.0.1",
"netmask": "255.0.0.0",
"family": "IPv4"
},
{
"name": "wlp3s0",
"internal": false,
"mac": "7c:b0:c2:fe:93:86",
"address": "192.168.43.5",
"netmask": "255.255.255.0",
"family": "IPv4"
},
{
"name": "docker0",
"internal": false,
"mac": "02:42:64:b5:eb:c5",
"address": "172.17.0.1",
"netmask": "255.255.0.0",
"family": "IPv4"
},
{
"name": "br-e929893879ec",
"internal": false,
"mac": "02:42:77:2b:e8:70",
"address": "172.19.0.1",
"netmask": "255.255.0.0",
"family": "IPv4"
},
{
"name": "lo",
"internal": true,
"mac": "00:00:00:00:00:00",
"address": "::1",
"netmask": "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
"family": "IPv6",
"scopeid": 0
},
{
"name": "wlp3s0",
"internal": false,
"mac": "7c:b0:c2:fe:93:86",
"address": "2600:100b:b011:583b:7eb0:c2ff:fefe:9386",
"netmask": "ffff:ffff:ffff:ffff::",
"family": "IPv6",
"scopeid": 0
},
{
"name": "wlp3s0",
"internal": false,
"mac": "7c:b0:c2:fe:93:86",
"address": "fe80::7eb0:c2ff:fefe:9386",
"netmask": "ffff:ffff:ffff:ffff::",
"family": "IPv6",
"scopeid": 3
},
{
"name": "docker0",
"internal": false,
"mac": "02:42:64:b5:eb:c5",
"address": "fe80::42:64ff:feb5:ebc5",
"netmask": "ffff:ffff:ffff:ffff::",
"family": "IPv6",
"scopeid": 5
},
{
"name": "br-e929893879ec",
"internal": false,
"mac": "02:42:77:2b:e8:70",
"address": "fe80::42:77ff:fe2b:e870",
"netmask": "ffff:ffff:ffff:ffff::",
"family": "IPv6",
"scopeid": 6
},
{
"name": "veth9f2842e",
"internal": false,
"mac": "02:97:03:9d:e0:38",
"address": "fe80::97:3ff:fe9d:e038",
"netmask": "ffff:ffff:ffff:ffff::",
"family": "IPv6",
"scopeid": 8
},
{
"name": "veth009c2b9",
"internal": false,
"mac": "e2:7b:8b:06:cb:6a",
"address": "fe80::e07b:8bff:fe06:cb6a",
"netmask": "ffff:ffff:ffff:ffff::",
"family": "IPv6",
"scopeid": 10
},
{
"name": "vethcfe1d35",
"internal": false,
"mac": "de:ee:14:2d:7e:e7",
"address": "fe80::dcee:14ff:fe2d:7ee7",
"netmask": "ffff:ffff:ffff:ffff::",
"family": "IPv6",
"scopeid": 12
},
{
"name": "vethff03ba3",
"internal": false,
"mac": "0e:99:63:87:38:fc",
"address": "fe80::c99:63ff:fe87:38fc",
"netmask": "ffff:ffff:ffff:ffff::",
"family": "IPv6",
"scopeid": 14
}
],
"host": "archlinux"
},
"javascriptStack": {
"message": "No stack.",
"stack": [
"Unavailable."
]
},
"nativeStack": [
{
"pc": "0x000055daf3226e8a",
"symbol": "report::TriggerNodeReport(v8::Isolate*, node::Environment*, char const*, char const*, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, v8::Local<v8::String>) [/usr/bin/node]"
},
{
"pc": "0x000055daf30e7d48",
"symbol": "node::OnFatalError(char const*, char const*) [/usr/bin/node]"
},
{
"pc": "0x000055daf325b382",
"symbol": "v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [/usr/bin/node]"
},
{
"pc": "0x000055daf325b5e8",
"symbol": "v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [/usr/bin/node]"
},
{
"pc": "0x000055daf33e5906",
"symbol": " [/usr/bin/node]"
},
{
"pc": "0x000055daf33e5a49",
"symbol": " [/usr/bin/node]"
},
{
"pc": "0x000055daf33f822d",
"symbol": "v8::internal::Heap::PerformGarbageCollection(v8::internal::GarbageCollector, v8::GCCallbackFlags) [/usr/bin/node]"
},
{
"pc": "0x000055daf33f8f58",
"symbol": "v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [/usr/bin/node]"
},
{
"pc": "0x000055daf33fb48c",
"symbol": "v8::internal::Heap::AllocateRawWithLightRetrySlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [/usr/bin/node]"
},
{
"pc": "0x000055daf33fb4f4",
"symbol": "v8::internal::Heap::AllocateRawWithRetryOrFailSlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [/usr/bin/node]"
},
{
"pc": "0x000055daf33c0396",
"symbol": "v8::internal::Factory::AllocateRawWithImmortalMap(int, v8::internal::AllocationType, v8::internal::Map, v8::internal::AllocationAlignment) [/usr/bin/node]"
},
{
"pc": "0x000055daf33c8fd0",
"symbol": "v8::internal::Factory::NewRawTwoByteString(int, v8::internal::AllocationType) [/usr/bin/node]"
},
{
"pc": "0x000055daf360c29e",
"symbol": "v8::internal::String::SlowFlatten(v8::internal::Isolate*, v8::internal::Handle<v8::internal::ConsString>, v8::internal::AllocationType) [/usr/bin/node]"
},
{
"pc": "0x000055daf360d86c",
"symbol": "v8::internal::String::SlowEquals(v8::internal::Isolate*, v8::internal::Handle<v8::internal::String>, v8::internal::Handle<v8::internal::String>) [/usr/bin/node]"
},
{
"pc": "0x000055daf37375ab",
"symbol": "v8::internal::Runtime_StringEqual(int, unsigned long*, v8::internal::Isolate*) [/usr/bin/node]"
},
{
"pc": "0x000055daf3a4abd9",
"symbol": " [/usr/bin/node]"
}
],
"javascriptHeap": {
"totalMemory": 2152394752,
"totalCommittedMemory": 2150767368,
"usedMemory": 2135044960,
"availableMemory": 47808640,
"memoryLimit": 2197815296,
"heapSpaces": {
"read_only_space": {
"memorySize": 262144,
"committedMemory": 33328,
"capacity": 33040,
"used": 33040,
"available": 0
},
"new_space": {
"memorySize": 2097152,
"committedMemory": 1114192,
"capacity": 1047424,
"used": 66000,
"available": 981424
},
"old_space": {
"memorySize": 2049843200,
"committedMemory": 2049732712,
"capacity": 2037853328,
"used": 2037494080,
"available": 359248
},
"code_space": {
"memorySize": 10129408,
"committedMemory": 9828000,
"capacity": 8619200,
"used": 8619200,
"available": 0
},
"map_space": {
"memorySize": 1052672,
"committedMemory": 1048960,
"capacity": 735040,
"used": 735040,
"available": 0
},
"large_object_space": {
"memorySize": 88633344,
"committedMemory": 88633344,
"capacity": 87782784,
"used": 87782784,
"available": 0
},
"code_large_object_space": {
"memorySize": 376832,
"committedMemory": 376832,
"capacity": 314816,
"used": 314816,
"available": 0
},
"new_large_object_space": {
"memorySize": 0,
"committedMemory": 0,
"capacity": 1047424,
"used": 0,
"available": 1047424
}
}
},
"resourceUsage": {
"userCpuSeconds": 2203.37,
"kernelCpuSeconds": 38.1817,
"cpuConsumptionPercent": 34.1127,
"maxRss": 2532282368,
"pageFaults": {
"IORequired": 22,
"IONotRequired": 7984554
},
"fsActivity": {
"reads": 194080,
"writes": 24
}
},
"uvthreadResourceUsage": {
"userCpuSeconds": 1428.9,
"kernelCpuSeconds": 15.9159,
"cpuConsumptionPercent": 21.9878,
"fsActivity": {
"reads": 194080,
"writes": 24
}
},
"libuv": [
],
"environmentVariables": {
"COLORTERM": "truecolor",
"DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/1000/bus",
"DESKTOP_SESSION": "awesome",
"DISPLAY": ":0.0",
"EDITOR": "nano",
"FZF_DEFAULT_COMMAND": "ag --hidden --ignore .git -g \"\"",
"GDMSESSION": "awesome",
"GO111MODULE": "on",
"GOBIN": "/home/jordan/go/bin",
"GREP_COLOR": "37;45",
"GREP_COLORS": "mt=37;45",
"GTK_MODULES": "canberra-gtk-module",
"HOME": "/home/jordan",
"LANG": "C",
"LESS": "-F -g -i -M -R -S -w -X -z-4",
"LESS_TERMCAP_mb": "\u001b[01;31m",
"LESS_TERMCAP_md": "\u001b[01;31m",
"LESS_TERMCAP_me": "\u001b[0m",
"LESS_TERMCAP_se": "\u001b[0m",
"LESS_TERMCAP_so": "\u001b[00;47;30m",
"LESS_TERMCAP_ue": "\u001b[0m",
"LESS_TERMCAP_us": "\u001b[01;32m",
"LOGNAME": "jordan",
"LS_COLORS": "rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.zst=01;31:*.tzst=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.wim=01;31:*.swm=01;31:*.dwm=01;31:*.esd=01;31:*.jpg=01;35:*.jpeg=01;35:*.mjpg=01;35:*.mjpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.m4a=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.oga=00;36:*.opus=00;36:*.spx=00;36:*.xspf=00;36:",
"MAIL": "/var/spool/mail/jordan",
"OLDPWD": "/home/jordan/Projects/project-citadel/web",
"PAGER": "less",
"PATH": "/home/jordan/.local/bin:/usr/local/bin:/usr/local/sbin:/home/nightwolf/Programs/cmake/bin:/home/nightwolf/Programs/idea-IU-163.13906.18/bin:/home/nightwolf/Programs/wpcli:/home/nightwolf/neovim/bin:/home/nightwolf/Programs/Postman:/home/nightwolf/Programs/Android_SDK/tools/bin:/home/nightwolf/Development/PhantomJS/bin:/home/nightwolf/Programs/node/bin:/home/nightwolf/pyenv/bin:/home/nightwolf/Programs/vv:/usr/bin:/bin:/usr/lib/jvm/default/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl:/usr/local/go/bin:/home/jordan/go/bin:/home/jordan/.garden/bin:~/Programs/node/bin:~/.utilities:/home/jordan/.fzf/bin:/home/jordan/.gem/ruby/2.6.0/bin:/home/jordan/.garden/bin:~/Programs/node/bin:~/.utilities:/home/jordan/.gem/ruby/2.6.0/bin",
"PWD": "/home/jordan/Projects/project-citadel/web",
"SHELL": "/usr/bin/zsh",
"SHLVL": "2",
"SSH_AGENT_PID": "2150",
"SSH_AUTH_SOCK": "/tmp/ssh-agent.sock.1000",
"TERM": "xterm-256color",
"TMUX": "/tmp//tmux-1000/default,2216,1",
"TMUX_PANE": "%2",
"USER": "jordan",
"VIRTUALENVWRAPPER_PYTHON": "/usr/bin/python3",
"VIRTUAL_ENV_DISABLE_PROMPT": "12",
"VISUAL": "nano",
"VTE_VERSION": "5602",
"WINDOWID": "6291459",
"WORKON_HOME": "/home/jordan/.virtualenvs",
"XAUTHORITY": "/home/jordan/.Xauthority",
"XDG_GREETER_DATA_DIR": "/var/lib/lightdm-data/jordan",
"XDG_RUNTIME_DIR": "/run/user/1000",
"XDG_SEAT": "seat0",
"XDG_SEAT_PATH": "/org/freedesktop/DisplayManager/Seat0",
"XDG_SESSION_CLASS": "user",
"XDG_SESSION_DESKTOP": "awesome",
"XDG_SESSION_ID": "1",
"XDG_SESSION_PATH": "/org/freedesktop/DisplayManager/Session0",
"XDG_SESSION_TYPE": "x11",
"XDG_VTNR": "7",
"_": "/usr/bin/nvim",
"is_vim": "ps -o state= -o comm= -t '#{pane_tty}' | grep -iqE '^[^TXZ ]+ +(\\S+\\/)?g?(view|n?vim?x?)(diff)?$'",
"tmux_version": "$(tmux -V | sed -En \"s/^tmux ([0-9]+(.[0-9]+)?).*/\\1/p\")",
"LC_MESSAGES": "",
"VIMRUNTIME": "/usr/share/nvim/runtime",
"NVIM_LISTEN_ADDRESS": "/tmp/nvimGEyAE0/0",
"MYVIMRC": "/home/jordan/.config/nvim/init.vim",
"COC_VIMCONFIG": "/home/jordan/.config/nvim",
"COC_DATA_HOME": "/home/jordan/.config/coc",
"TSS_LOG": "-level verbose -file /tmp/coc-nvim-tsc.log",
"NODE_PATH": "/home/jordan/Projects/project-citadel/web/node_modules"
},
"userLimits": {
"core_file_size_blocks": {
"soft": "unlimited",
"hard": "unlimited"
},
"data_seg_size_kbytes": {
"soft": "unlimited",
"hard": "unlimited"
},
"file_size_blocks": {
"soft": "unlimited",
"hard": "unlimited"
},
"max_locked_memory_bytes": {
"soft": 65536,
"hard": 65536
},
"max_memory_size_kbytes": {
"soft": "unlimited",
"hard": "unlimited"
},
"open_files": {
"soft": 524288,
"hard": 524288
},
"stack_size_bytes": {
"soft": 8388608,
"hard": "unlimited"
},
"cpu_time_seconds": {
"soft": "unlimited",
"hard": "unlimited"
},
"max_user_processes": {
"soft": 31138,
"hard": 31138
},
"virtual_memory_kbytes": {
"soft": "unlimited",
"hard": "unlimited"
}
},
"sharedObjects": [
"linux-vdso.so.1",
"/usr/lib/libz.so.1",
"/usr/lib/libcares.so.2",
"/usr/lib/libnghttp2.so.14",
"/usr/lib/libcrypto.so.1.1",
"/usr/lib/libssl.so.1.1",
"/usr/lib/libicui18n.so.65",
"/usr/lib/libicuuc.so.65",
"/usr/lib/libdl.so.2",
"/usr/lib/libstdc++.so.6",
"/usr/lib/libm.so.6",
"/usr/lib/libgcc_s.so.1",
"/usr/lib/libpthread.so.0",
"/usr/lib/libc.so.6",
"/usr/lib/libicudata.so.65",
"/lib64/ld-linux-x86-64.so.2"
]
}

View File

@ -0,0 +1,498 @@
{
"header": {
"reportVersion": 1,
"event": "Allocation failed - JavaScript heap out of memory",
"trigger": "FatalError",
"filename": "report.20200703.192901.99868.0.001.json",
"dumpEventTime": "2020-07-03T19:29:01Z",
"dumpEventTimeStamp": "1593822541435",
"processId": 99868,
"cwd": "/home/jordan/Projects/project-citadel/web",
"commandLine": [
"/usr/bin/node",
"/home/jordan/.config/coc/extensions/node_modules/coc-tsserver/bin/tsserverForkStart",
"/home/jordan/Projects/project-citadel/web/node_modules/typescript/lib/tsserver.js",
"--allowLocalPluginLoads",
"--useInferredProjectPerProjectRoot",
"--cancellationPipeName",
"/tmp/coc-nvim-tscancellation-c8c6c50b271560f15471.sock*",
"--npmLocation",
"\"/usr/bin/npm\"",
"--noGetErrOnBackgroundUpdate",
"--validateDefaultNpmLocation"
],
"nodejsVersion": "v13.8.0",
"glibcVersionRuntime": "2.30",
"glibcVersionCompiler": "2.30",
"wordSize": 64,
"arch": "x64",
"platform": "linux",
"componentVersions": {
"node": "13.8.0",
"v8": "7.9.317.25-node.28",
"uv": "1.34.1",
"zlib": "1.2.11",
"brotli": "1.0.7",
"ares": "1.15.0",
"modules": "79",
"nghttp2": "1.39.2",
"napi": "5",
"llhttp": "2.0.4",
"openssl": "1.1.1d",
"cldr": "36.0",
"icu": "65.1",
"tz": "2019c",
"unicode": "12.1"
},
"release": {
"name": "node",
"headersUrl": "https://nodejs.org/download/release/v13.8.0/node-v13.8.0-headers.tar.gz",
"sourceUrl": "https://nodejs.org/download/release/v13.8.0/node-v13.8.0.tar.gz"
},
"osName": "Linux",
"osRelease": "4.19.101-1-lts",
"osVersion": "#1 SMP Sat, 01 Feb 2020 16:35:36 +0000",
"osMachine": "x86_64",
"cpus": [
{
"model": "Intel(R) Core(TM) i7-6500U CPU @ 2.50GHz",
"speed": 2800,
"user": 49033300,
"nice": 14200,
"sys": 10239800,
"idle": 199777400,
"irq": 1327100
},
{
"model": "Intel(R) Core(TM) i7-6500U CPU @ 2.50GHz",
"speed": 2800,
"user": 46034200,
"nice": 20700,
"sys": 9673500,
"idle": 173927400,
"irq": 721000
},
{
"model": "Intel(R) Core(TM) i7-6500U CPU @ 2.50GHz",
"speed": 2800,
"user": 46463200,
"nice": 15400,
"sys": 9416800,
"idle": 174061400,
"irq": 597100
},
{
"model": "Intel(R) Core(TM) i7-6500U CPU @ 2.50GHz",
"speed": 2800,
"user": 46337200,
"nice": 14900,
"sys": 9474300,
"idle": 174305900,
"irq": 487800
}
],
"networkInterfaces": [
{
"name": "lo",
"internal": true,
"mac": "00:00:00:00:00:00",
"address": "127.0.0.1",
"netmask": "255.0.0.0",
"family": "IPv4"
},
{
"name": "wlp3s0",
"internal": false,
"mac": "7c:b0:c2:fe:93:86",
"address": "192.168.43.5",
"netmask": "255.255.255.0",
"family": "IPv4"
},
{
"name": "docker0",
"internal": false,
"mac": "02:42:84:c7:8c:b4",
"address": "172.17.0.1",
"netmask": "255.255.0.0",
"family": "IPv4"
},
{
"name": "br-e929893879ec",
"internal": false,
"mac": "02:42:c6:97:95:8c",
"address": "172.19.0.1",
"netmask": "255.255.0.0",
"family": "IPv4"
},
{
"name": "lo",
"internal": true,
"mac": "00:00:00:00:00:00",
"address": "::1",
"netmask": "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
"family": "IPv6",
"scopeid": 0
},
{
"name": "wlp3s0",
"internal": false,
"mac": "7c:b0:c2:fe:93:86",
"address": "2600:100b:b018:172f:7eb0:c2ff:fefe:9386",
"netmask": "ffff:ffff:ffff:ffff::",
"family": "IPv6",
"scopeid": 0
},
{
"name": "wlp3s0",
"internal": false,
"mac": "7c:b0:c2:fe:93:86",
"address": "2600:100b:b009:7105:7eb0:c2ff:fefe:9386",
"netmask": "ffff:ffff:ffff:ffff::",
"family": "IPv6",
"scopeid": 0
},
{
"name": "wlp3s0",
"internal": false,
"mac": "7c:b0:c2:fe:93:86",
"address": "fe80::7eb0:c2ff:fefe:9386",
"netmask": "ffff:ffff:ffff:ffff::",
"family": "IPv6",
"scopeid": 3
},
{
"name": "docker0",
"internal": false,
"mac": "02:42:84:c7:8c:b4",
"address": "fe80::42:84ff:fec7:8cb4",
"netmask": "ffff:ffff:ffff:ffff::",
"family": "IPv6",
"scopeid": 5
},
{
"name": "br-e929893879ec",
"internal": false,
"mac": "02:42:c6:97:95:8c",
"address": "fe80::42:c6ff:fe97:958c",
"netmask": "ffff:ffff:ffff:ffff::",
"family": "IPv6",
"scopeid": 6
},
{
"name": "veth861c75f",
"internal": false,
"mac": "de:7f:bb:fc:33:a0",
"address": "fe80::dc7f:bbff:fefc:33a0",
"netmask": "ffff:ffff:ffff:ffff::",
"family": "IPv6",
"scopeid": 8
},
{
"name": "veth40b666f",
"internal": false,
"mac": "5e:46:25:1a:79:73",
"address": "fe80::5c46:25ff:fe1a:7973",
"netmask": "ffff:ffff:ffff:ffff::",
"family": "IPv6",
"scopeid": 10
},
{
"name": "vetha03af75",
"internal": false,
"mac": "7e:23:c2:9d:e8:db",
"address": "fe80::7c23:c2ff:fe9d:e8db",
"netmask": "ffff:ffff:ffff:ffff::",
"family": "IPv6",
"scopeid": 12
},
{
"name": "vethb492e80",
"internal": false,
"mac": "de:af:a3:79:1e:9a",
"address": "fe80::dcaf:a3ff:fe79:1e9a",
"netmask": "ffff:ffff:ffff:ffff::",
"family": "IPv6",
"scopeid": 14
}
],
"host": "archlinux"
},
"javascriptStack": {
"message": "No stack.",
"stack": [
"Unavailable."
]
},
"nativeStack": [
{
"pc": "0x000055c2d7ceae8a",
"symbol": "report::TriggerNodeReport(v8::Isolate*, node::Environment*, char const*, char const*, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, v8::Local<v8::String>) [/usr/bin/node]"
},
{
"pc": "0x000055c2d7babd48",
"symbol": "node::OnFatalError(char const*, char const*) [/usr/bin/node]"
},
{
"pc": "0x000055c2d7d1f382",
"symbol": "v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [/usr/bin/node]"
},
{
"pc": "0x000055c2d7d1f5e8",
"symbol": "v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [/usr/bin/node]"
},
{
"pc": "0x000055c2d7ea9906",
"symbol": " [/usr/bin/node]"
},
{
"pc": "0x000055c2d7ea9a49",
"symbol": " [/usr/bin/node]"
},
{
"pc": "0x000055c2d7ebc22d",
"symbol": "v8::internal::Heap::PerformGarbageCollection(v8::internal::GarbageCollector, v8::GCCallbackFlags) [/usr/bin/node]"
},
{
"pc": "0x000055c2d7ebcf58",
"symbol": "v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [/usr/bin/node]"
},
{
"pc": "0x000055c2d7ebf48c",
"symbol": "v8::internal::Heap::AllocateRawWithLightRetrySlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [/usr/bin/node]"
},
{
"pc": "0x000055c2d7ebf4f4",
"symbol": "v8::internal::Heap::AllocateRawWithRetryOrFailSlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [/usr/bin/node]"
},
{
"pc": "0x000055c2d7e8499b",
"symbol": "v8::internal::Factory::NewFillerObject(int, bool, v8::internal::AllocationType, v8::internal::AllocationOrigin) [/usr/bin/node]"
},
{
"pc": "0x000055c2d81b5540",
"symbol": "v8::internal::Runtime_AllocateInYoungGeneration(int, unsigned long*, v8::internal::Isolate*) [/usr/bin/node]"
},
{
"pc": "0x000055c2d850ebd9",
"symbol": " [/usr/bin/node]"
}
],
"javascriptHeap": {
"totalMemory": 2152599552,
"totalCommittedMemory": 2151011168,
"usedMemory": 2136228312,
"availableMemory": 47169120,
"memoryLimit": 2197815296,
"heapSpaces": {
"read_only_space": {
"memorySize": 262144,
"committedMemory": 33328,
"capacity": 33040,
"used": 33040,
"available": 0
},
"new_space": {
"memorySize": 2097152,
"committedMemory": 1353624,
"capacity": 1047424,
"used": 336544,
"available": 710880
},
"old_space": {
"memorySize": 2037784576,
"committedMemory": 2037430360,
"capacity": 2026077792,
"used": 2025882720,
"available": 195072
},
"code_space": {
"memorySize": 6721536,
"committedMemory": 6463424,
"capacity": 5718144,
"used": 5718144,
"available": 0
},
"map_space": {
"memorySize": 1052672,
"committedMemory": 1048960,
"capacity": 737680,
"used": 737680,
"available": 0
},
"large_object_space": {
"memorySize": 104304640,
"committedMemory": 104304640,
"capacity": 103205528,
"used": 103205528,
"available": 0
},
"code_large_object_space": {
"memorySize": 376832,
"committedMemory": 376832,
"capacity": 314656,
"used": 314656,
"available": 0
},
"new_large_object_space": {
"memorySize": 0,
"committedMemory": 0,
"capacity": 1047424,
"used": 0,
"available": 1047424
}
}
},
"resourceUsage": {
"userCpuSeconds": 2108.75,
"kernelCpuSeconds": 34.8424,
"cpuConsumptionPercent": 31.044,
"maxRss": 2328186880,
"pageFaults": {
"IORequired": 32,
"IONotRequired": 6292470
},
"fsActivity": {
"reads": 250312,
"writes": 24
}
},
"uvthreadResourceUsage": {
"userCpuSeconds": 1298.13,
"kernelCpuSeconds": 16.4946,
"cpuConsumptionPercent": 19.0388,
"fsActivity": {
"reads": 250312,
"writes": 24
}
},
"libuv": [
],
"environmentVariables": {
"COLORTERM": "truecolor",
"DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/1000/bus",
"DESKTOP_SESSION": "awesome",
"DISPLAY": ":0.0",
"EDITOR": "nano",
"FZF_DEFAULT_COMMAND": "ag --hidden --ignore .git -g \"\"",
"GDMSESSION": "awesome",
"GO111MODULE": "on",
"GOBIN": "/home/jordan/go/bin",
"GREP_COLOR": "37;45",
"GREP_COLORS": "mt=37;45",
"GTK_MODULES": "canberra-gtk-module",
"HOME": "/home/jordan",
"LANG": "C",
"LESS": "-F -g -i -M -R -S -w -X -z-4",
"LESS_TERMCAP_mb": "\u001b[01;31m",
"LESS_TERMCAP_md": "\u001b[01;31m",
"LESS_TERMCAP_me": "\u001b[0m",
"LESS_TERMCAP_se": "\u001b[0m",
"LESS_TERMCAP_so": "\u001b[00;47;30m",
"LESS_TERMCAP_ue": "\u001b[0m",
"LESS_TERMCAP_us": "\u001b[01;32m",
"LOGNAME": "jordan",
"LS_COLORS": "rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.zst=01;31:*.tzst=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.wim=01;31:*.swm=01;31:*.dwm=01;31:*.esd=01;31:*.jpg=01;35:*.jpeg=01;35:*.mjpg=01;35:*.mjpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.m4a=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.oga=00;36:*.opus=00;36:*.spx=00;36:*.xspf=00;36:",
"MAIL": "/var/spool/mail/jordan",
"OLDPWD": "/home/jordan/Projects/project-citadel/web",
"PAGER": "less",
"PATH": "/home/jordan/.local/bin:/usr/local/bin:/usr/local/sbin:/home/nightwolf/Programs/cmake/bin:/home/nightwolf/Programs/idea-IU-163.13906.18/bin:/home/nightwolf/Programs/wpcli:/home/nightwolf/neovim/bin:/home/nightwolf/Programs/Postman:/home/nightwolf/Programs/Android_SDK/tools/bin:/home/nightwolf/Development/PhantomJS/bin:/home/nightwolf/Programs/node/bin:/home/nightwolf/pyenv/bin:/home/nightwolf/Programs/vv:/usr/bin:/bin:/usr/lib/jvm/default/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl:/usr/local/go/bin:/home/jordan/go/bin:/home/jordan/.garden/bin:~/Programs/node/bin:~/.utilities:/home/jordan/.fzf/bin:/home/jordan/.gem/ruby/2.6.0/bin:/home/jordan/.garden/bin:~/Programs/node/bin:~/.utilities:/home/jordan/.gem/ruby/2.6.0/bin",
"PWD": "/home/jordan/Projects/project-citadel/web",
"SHELL": "/usr/bin/zsh",
"SHLVL": "2",
"SSH_AGENT_PID": "1475",
"SSH_AUTH_SOCK": "/tmp/ssh-agent.sock.1000",
"TERM": "xterm-256color",
"TMUX": "/tmp//tmux-1000/default,1930,1",
"TMUX_PANE": "%3",
"USER": "jordan",
"VIRTUALENVWRAPPER_PYTHON": "/usr/bin/python3",
"VIRTUAL_ENV_DISABLE_PROMPT": "12",
"VISUAL": "nano",
"VTE_VERSION": "5602",
"WINDOWID": "6291459",
"WORKON_HOME": "/home/jordan/.virtualenvs",
"XAUTHORITY": "/home/jordan/.Xauthority",
"XDG_GREETER_DATA_DIR": "/var/lib/lightdm-data/jordan",
"XDG_RUNTIME_DIR": "/run/user/1000",
"XDG_SEAT": "seat0",
"XDG_SEAT_PATH": "/org/freedesktop/DisplayManager/Seat0",
"XDG_SESSION_CLASS": "user",
"XDG_SESSION_DESKTOP": "awesome",
"XDG_SESSION_ID": "1",
"XDG_SESSION_PATH": "/org/freedesktop/DisplayManager/Session0",
"XDG_SESSION_TYPE": "x11",
"XDG_VTNR": "7",
"_": "/usr/bin/nvim",
"is_vim": "ps -o state= -o comm= -t '#{pane_tty}' | grep -iqE '^[^TXZ ]+ +(\\S+\\/)?g?(view|n?vim?x?)(diff)?$'",
"tmux_version": "$(tmux -V | sed -En \"s/^tmux ([0-9]+(.[0-9]+)?).*/\\1/p\")",
"LC_MESSAGES": "",
"VIMRUNTIME": "/usr/share/nvim/runtime",
"NVIM_LISTEN_ADDRESS": "/tmp/nvimerL06q/0",
"MYVIMRC": "/home/jordan/.config/nvim/init.vim",
"COC_VIMCONFIG": "/home/jordan/.config/nvim",
"COC_DATA_HOME": "/home/jordan/.config/coc",
"TSS_LOG": "-level verbose -file /tmp/coc-nvim-tsc.log",
"NODE_PATH": "/home/jordan/Projects/project-citadel/web/node_modules"
},
"userLimits": {
"core_file_size_blocks": {
"soft": "unlimited",
"hard": "unlimited"
},
"data_seg_size_kbytes": {
"soft": "unlimited",
"hard": "unlimited"
},
"file_size_blocks": {
"soft": "unlimited",
"hard": "unlimited"
},
"max_locked_memory_bytes": {
"soft": 65536,
"hard": 65536
},
"max_memory_size_kbytes": {
"soft": "unlimited",
"hard": "unlimited"
},
"open_files": {
"soft": 524288,
"hard": 524288
},
"stack_size_bytes": {
"soft": 8388608,
"hard": "unlimited"
},
"cpu_time_seconds": {
"soft": "unlimited",
"hard": "unlimited"
},
"max_user_processes": {
"soft": 31137,
"hard": 31137
},
"virtual_memory_kbytes": {
"soft": "unlimited",
"hard": "unlimited"
}
},
"sharedObjects": [
"linux-vdso.so.1",
"/usr/lib/libz.so.1",
"/usr/lib/libcares.so.2",
"/usr/lib/libnghttp2.so.14",
"/usr/lib/libcrypto.so.1.1",
"/usr/lib/libssl.so.1.1",
"/usr/lib/libicui18n.so.65",
"/usr/lib/libicuuc.so.65",
"/usr/lib/libdl.so.2",
"/usr/lib/libstdc++.so.6",
"/usr/lib/libm.so.6",
"/usr/lib/libgcc_s.so.1",
"/usr/lib/libpthread.so.0",
"/usr/lib/libc.so.6",
"/usr/lib/libicudata.so.65",
"/lib64/ld-linux-x86-64.so.2"
]
}

View File

@ -1,20 +1,57 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import Admin from 'shared/components/Admin';
import Select from 'shared/components/Select';
import GlobalTopNavbar from 'App/TopNavbar';
import { useUsersQuery, useCreateUserAccountMutation, UsersDocument } from 'shared/generated/graphql';
import {
useUsersQuery,
useDeleteUserAccountMutation,
useCreateUserAccountMutation,
UsersDocument,
UsersQuery,
} from 'shared/generated/graphql';
import Input from 'shared/components/Input';
import styled from 'styled-components';
import Button from 'shared/components/Button';
import { useForm } from 'react-hook-form';
import { usePopup, Popup } from 'shared/components/PopupMenu';
import produce from 'immer';
import updateApolloCache from 'shared/utils/cache';
const DeleteUserWrapper = styled.div`
display: flex;
flex-direction: column;
`;
const DeleteUserDescription = styled.p`
font-size: 14px;
`;
const DeleteUserButton = styled(Button)`
margin-top: 6px;
padding: 6px 12px;
width: 100%;
`;
type DeleteUserPopupProps = {
onDeleteUser: () => void;
};
const DeleteUserPopup: React.FC<DeleteUserPopupProps> = ({ onDeleteUser }) => {
return (
<DeleteUserWrapper>
<DeleteUserDescription>Deleting this user will remove all user related data.</DeleteUserDescription>
<DeleteUserButton onClick={() => onDeleteUser()} color="danger">
Delete user
</DeleteUserButton>
</DeleteUserWrapper>
);
};
type CreateUserData = {
email: string;
username: string;
fullName: string;
initials: string;
password: string;
roleCode: string;
};
const CreateUserForm = styled.form`
display: flex;
@ -34,11 +71,16 @@ const InputError = styled.span`
color: rgba(${props => props.theme.colors.danger});
font-size: 12px;
`;
type AddUserPopupProps = {
onAddUser: (user: CreateUserData) => void;
};
const AddUserPopup: React.FC<AddUserPopupProps> = ({ onAddUser }) => {
const { register, handleSubmit, errors } = useForm<CreateUserData>();
const { register, handleSubmit, errors, setValue } = useForm<CreateUserData>();
const [role, setRole] = useState<string | null>(null);
register({ name: 'roleCode' }, { required: true });
const createUser = (data: CreateUserData) => {
onAddUser(data);
};
@ -63,6 +105,18 @@ const AddUserPopup: React.FC<AddUserPopupProps> = ({ onAddUser }) => {
variant="alternate"
ref={register({ required: 'Email is required' })}
/>
<Select
label="Role"
value={role}
options={[
{ label: 'Admin', value: 'admin' },
{ label: 'Member', value: 'member' },
]}
onChange={newRole => {
setRole(newRole);
setValue('roleCode', newRole.value);
}}
/>
{errors.email && <InputError>{errors.email.message}</InputError>}
<AddUserInput
floatingLabel
@ -105,6 +159,15 @@ const AdminRoute = () => {
}, []);
const { loading, data } = useUsersQuery();
const { showPopup, hidePopup } = usePopup();
const [deleteUser] = useDeleteUserAccountMutation({
update: (client, response) => {
updateApolloCache<UsersQuery>(client, UsersDocument, cache =>
produce(cache, draftCache => {
draftCache.users = cache.users.filter(u => u.id !== response.data.deleteUserAccount.userAccount.id);
}),
);
},
});
const [createUser] = useCreateUserAccountMutation({
update: (client, createData) => {
const cacheData: any = client.readQuery({
@ -133,8 +196,21 @@ const AdminRoute = () => {
<GlobalTopNavbar projectID={null} onSaveProjectName={() => {}} name={null} />
<Admin
initialTab={1}
users={data.users.map((user: any) => ({ ...user, role: 'TBD' }))}
users={data.users}
onInviteUser={() => {}}
onDeleteUser={($target, userID) => {
showPopup(
$target,
<Popup tab={0} title="Delete user?" onClose={() => hidePopup()}>
<DeleteUserPopup
onDeleteUser={() => {
deleteUser({ variables: { userID } });
hidePopup();
}}
/>
</Popup>,
);
}}
onAddUser={$target => {
showPopup(
$target,

View File

@ -1,11 +1,12 @@
import React, { useState, useContext, useEffect } from 'react';
import TopNavbar from 'shared/components/TopNavbar';
import TopNavbar, { MenuItem } from 'shared/components/TopNavbar';
import styled from 'styled-components/macro';
import DropdownMenu, { ProfileMenu } from 'shared/components/DropdownMenu';
import ProjectSettings, { DeleteConfirm, DELETE_INFO } from 'shared/components/ProjectSettings';
import { useHistory } from 'react-router';
import UserIDContext from 'App/context';
import {
RoleCode,
useMeQuery,
useDeleteProjectMutation,
useGetProjectsQuery,
@ -219,32 +220,38 @@ export const ProjectPopup: React.FC<ProjectPopupProps> = ({ history, name, proje
type GlobalTopNavbarProps = {
nameOnly?: boolean;
projectID: string | null;
onChangeProjectOwner?: (userID: string) => void;
name: string | null;
initialTab?: number;
currentTab?: number;
popupContent?: JSX.Element;
menuType?: Array<string>;
menuType?: Array<MenuItem>;
onChangeRole?: (userID: string, roleCode: RoleCode) => void;
projectMembers?: null | Array<TaskUser>;
onSaveProjectName?: (projectName: string) => void;
onInviteUser?: ($target: React.RefObject<HTMLElement>) => void;
onSetTab?: (tab: number) => void;
onRemoveFromBoard?: (userID: string) => void;
};
const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
initialTab,
currentTab,
onSetTab,
menuType,
projectID,
onChangeProjectOwner,
onChangeRole,
name,
popupContent,
projectMembers,
onInviteUser,
onSaveProjectName,
onRemoveFromBoard,
nameOnly,
}) => {
console.log(popupContent);
const { loading, data } = useMeQuery();
const { showPopup, hidePopup, setTab } = usePopup();
const history = useHistory();
const [currentTab, setCurrentTab] = useState(initialTab);
useEffect(() => {
setCurrentTab(initialTab);
}, [initialTab]);
const { userID, setUserID } = useContext(UserIDContext);
const onLogout = () => {
fetch('http://localhost:3333/auth/logout', {
@ -305,7 +312,12 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
}}
currentTab={currentTab}
user={data ? data.me : null}
onInviteUser={onInviteUser}
onChangeRole={onChangeRole}
onChangeProjectOwner={onChangeProjectOwner}
onNotificationClick={() => {}}
onSetTab={onSetTab}
onRemoveFromBoard={onRemoveFromBoard}
onDashboardClick={() => {
history.push('/');
}}

View File

@ -53,7 +53,7 @@ const Projects = () => {
<GlobalTopNavbar projectID={null} onSaveProjectName={() => {}} name={null} />
{!loading && data && (
<Settings
profile={data.me.profileIcon}
profile={data.me}
onProfileAvatarChange={() => {
if ($fileUpload && $fileUpload.current) {
$fileUpload.current.click();

View File

@ -305,21 +305,20 @@ const Details: React.FC<DetailsProps> = ({
}}
onMemberProfile={($targetRef, memberID) => {
const member = data.findTask.assigned.find(m => m.id === memberID);
const profileIcon = member ? member.profileIcon : null;
showPopup(
$targetRef,
<Popup title={null} onClose={() => {}} tab={0}>
<MiniProfile
profileIcon={profileIcon}
displayName="Jordan Knott"
username="@jordanthedev"
bio="None"
onRemoveFromTask={() => {
unassignTask({ variables: { taskID: data.findTask.id, userID: userID ?? '' } });
}}
/>
</Popup>,
);
if (member) {
showPopup(
$targetRef,
<Popup title={null} onClose={() => {}} tab={0}>
<MiniProfile
user={member}
bio="None"
onRemoveFromTask={() => {
unassignTask({ variables: { taskID: data.findTask.id, userID: userID ?? '' } });
}}
/>
</Popup>,
);
}
}}
onOpenAddMemberPopup={(task, $targetRef) => {
showPopup(

View File

@ -6,8 +6,12 @@ import GlobalTopNavbar, { ProjectPopup } from 'App/TopNavbar';
import styled, { css } from 'styled-components/macro';
import { Bolt, ToggleOn, Tags, CheckCircle, Sort, Filter } from 'shared/icons';
import { usePopup, Popup } from 'shared/components/PopupMenu';
import { useParams, Route, useRouteMatch, useHistory, RouteComponentProps } from 'react-router-dom';
import { useParams, Route, useRouteMatch, useHistory, RouteComponentProps, useLocation } from 'react-router-dom';
import {
useSetProjectOwnerMutation,
useUpdateProjectMemberRoleMutation,
useCreateProjectMemberMutation,
useDeleteProjectMemberMutation,
useSetTaskCompleteMutation,
useToggleTaskLabelMutation,
useUpdateProjectNameMutation,
@ -30,6 +34,7 @@ import {
useUnassignTaskMutation,
useUpdateTaskDueDateMutation,
FindProjectQuery,
useUsersQuery,
} from 'shared/generated/graphql';
import TaskAssignee from 'shared/components/TaskAssignee';
@ -48,25 +53,51 @@ import Details from './Details';
import { useApolloClient } from '@apollo/react-hooks';
import UserIDContext from 'App/context';
import DueDateManager from 'shared/components/DueDateManager';
import Input from 'shared/components/Input';
import Member from 'shared/components/Member';
const getCacheData = (client: any, projectID: string) => {
const cacheData: FindProjectQuery = client.readQuery({
query: FindProjectDocument,
variables: {
projectId: projectID,
},
});
return cacheData;
const SearchInput = styled(Input)`
margin: 0;
`;
const UserMember = styled(Member)`
padding: 4px 0;
cursor: pointer;
&:hover {
background: rgba(${props => props.theme.colors.bg.primary}, 0.4);
}
border-radius: 6px;
`;
const MemberList = styled.div`
margin: 8px 0;
`;
type UserManagementPopupProps = {
users: Array<User>;
projectMembers: Array<TaskUser>;
onAddProjectMember: (userID: string) => void;
};
const writeCacheData = (client: any, projectID: string, cacheData: any, newData: any) => {
client.writeQuery({
query: FindProjectDocument,
variables: {
projectId: projectID,
},
data: { ...cacheData, findProject: newData },
});
const UserManagementPopup: React.FC<UserManagementPopupProps> = ({ users, projectMembers, onAddProjectMember }) => {
return (
<Popup tab={0} title="Invite a user">
<SearchInput width="100%" variant="alternate" placeholder="Email address or name" name="search" />
<MemberList>
{users
.filter(u => u.id !== projectMembers.find(p => p.id === u.id)?.id)
.map(user => (
<UserMember
key={user.id}
onCardMemberClick={() => onAddProjectMember(user.id)}
showName
member={user}
taskID=""
/>
))}
</MemberList>
</Popup>
);
};
type TaskRouteProps = {
@ -300,6 +331,7 @@ const Project = () => {
},
});
const [updateTaskGroupLocation] = useUpdateTaskGroupLocationMutation({});
const [updateProjectMemberRole] = useUpdateProjectMemberRoleMutation();
const [deleteTaskGroup] = useDeleteTaskGroupMutation({
onCompleted: deletedTaskGroupData => {},
@ -492,9 +524,39 @@ const Project = () => {
});
const [setTaskComplete] = useSetTaskCompleteMutation();
const [createProjectMember] = useCreateProjectMemberMutation({
update: (client, response) => {
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache =>
produce(cache, draftCache => {
draftCache.findProject.members.push({ ...response.data.createProjectMember.member });
}),
{ projectId: projectID },
);
},
});
const [setProjectOwner] = useSetProjectOwnerMutation();
const [deleteProjectMember] = useDeleteProjectMemberMutation({
update: (client, response) => {
updateApolloCache<FindProjectQuery>(
client,
FindProjectDocument,
cache =>
produce(cache, draftCache => {
draftCache.findProject.members = cache.findProject.members.filter(
m => m.id !== response.data.deleteProjectMember.member.id,
);
}),
{ projectId: projectID },
);
},
});
const client = useApolloClient();
const { userID } = useContext(UserIDContext);
const location = useLocation();
const { showPopup, hidePopup } = usePopup();
const $labelsRef = useRef<HTMLDivElement>(null);
@ -546,12 +608,35 @@ const Project = () => {
return (
<>
<GlobalTopNavbar
onChangeRole={(userID, roleCode) => {
updateProjectMemberRole({ variables: { userID, roleCode, projectID } });
}}
onChangeProjectOwner={uid => {
setProjectOwner({ variables: { ownerID: uid, projectID } });
hidePopup();
}}
onRemoveFromBoard={userID => {
deleteProjectMember({ variables: { userID, projectID } });
hidePopup();
}}
onSaveProjectName={projectName => {
updateProjectName({ variables: { projectID, name: projectName } });
}}
onInviteUser={$target => {
showPopup(
$target,
<UserManagementPopup
onAddProjectMember={userID => {
createProjectMember({ variables: { userID, projectID } });
}}
users={data.users}
projectMembers={data.findProject.members}
/>,
);
}}
popupContent={<ProjectPopup history={history} name={data.findProject.name} projectID={projectID} />}
menuType={MENU_TYPES.PROJECT_MENU}
initialTab={0}
menuType={[{ name: 'Board', link: location.pathname }]}
currentTab={0}
projectMembers={data.findProject.members}
projectID={projectID}
name={data.findProject.name}
@ -647,27 +732,18 @@ const Project = () => {
onCreateTaskGroup={onCreateList}
onCardMemberClick={($targetRef, taskID, memberID) => {
const member = data.findProject.members.find(m => m.id === memberID);
const profileIcon = member ? member.profileIcon : null;
showPopup(
$targetRef,
<Popup
title={null}
onClose={() => {
hidePopup();
}}
tab={0}
>
if (member) {
showPopup(
$targetRef,
<MiniProfile
profileIcon={profileIcon}
displayName="Jordan Knott"
username="@jordanthedev"
user={member}
bio="None"
onRemoveFromTask={() => {
/* unassignTask({ variables: { taskID: data.findTask.id, userID: userID ?? '' } }); */
}}
/>
</Popup>,
);
/>,
);
}
}}
onChangeTaskGroupName={(taskGroupID, name) => {
updateTaskGroupName({ variables: { taskGroupID, name } });
@ -715,21 +791,18 @@ const Project = () => {
}}
onCardMemberClick={($targetRef, taskID, memberID) => {
const member = data.findProject.members.find(m => m.id === memberID);
const profileIcon = member ? member.profileIcon : null;
showPopup(
$targetRef,
<Popup title={null} onClose={() => hidePopup()} tab={0}>
if (member) {
showPopup(
$targetRef,
<MiniProfile
profileIcon={profileIcon}
displayName="Jordan Knott"
username="@jordanthedev"
bio="None"
user={member}
onRemoveFromTask={() => {
/* unassignTask({ variables: { taskID: data.findTask.id, userID: userID ?? '' } }); */
}}
/>
</Popup>,
);
/>,
);
}
}}
onOpenLabelsPopup={($targetRef, task) => {
taskLabelsRef.current = task.labels;

View File

@ -1,6 +1,7 @@
import React, { useState, useContext, useEffect } from 'react';
import styled from 'styled-components/macro';
import GlobalTopNavbar from 'App/TopNavbar';
import Empty from 'shared/undraw/Empty';
import {
useCreateTeamMutation,
useGetProjectsQuery,
@ -20,7 +21,27 @@ import { useForm } from 'react-hook-form';
import Input from 'shared/components/Input';
import updateApolloCache from 'shared/utils/cache';
import produce from 'immer';
const EmptyStateContent = styled.div`
display: flex;
justy-content: center;
align-items: center;
flex-direction: column;
`;
const EmptyStateTitle = styled.h3`
color: #fff;
font-size: 18px;
`;
const EmptyStatePrompt = styled.span`
color: rgba(${props => props.theme.colors.text.primary});
font-size: 16px;
margin-top: 8px;
`;
const EmptyState = styled(Empty)`
display: block;
margin: 0 auto;
`;
const CreateTeamButton = styled(Button)`
width: 100%;
`;
@ -193,6 +214,10 @@ const AddTeamButton = styled(Button)`
top: 6px;
right: 12px;
`;
const CreateFirstTeam = styled(Button)`
margin-top: 8px;
`;
type ShowNewProject = {
open: boolean;
initialTeamID: null | string;
@ -277,6 +302,39 @@ const Projects = () => {
>
Add Team
</AddTeamButton>
{projectTeams.length === 0 && (
<EmptyStateContent>
<EmptyState width={425} height={425} />
<EmptyStateTitle>No teams exist</EmptyStateTitle>
<EmptyStatePrompt>Create a new team to get started</EmptyStatePrompt>
<CreateFirstTeam
variant="outline"
onClick={$target => {
showPopup(
$target,
<Popup
title="Create team"
tab={0}
onClose={() => {
hidePopup();
}}
>
<CreateTeamForm
onCreateTeam={teamName => {
if (organizationID) {
createTeam({ variables: { name: teamName, organizationID } });
hidePopup();
}
}}
/>
</Popup>,
);
}}
>
Create new team
</CreateFirstTeam>
</EmptyStateContent>
)}
{projectTeams.map(team => {
return (
<div key={team.id}>
@ -286,10 +344,10 @@ const Projects = () => {
<SectionActionLink to={`/teams/${team.id}`}>
<SectionAction variant="outline">Projects</SectionAction>
</SectionActionLink>
<SectionActionLink to="/">
<SectionActionLink to={`/teams/${team.id}/members`}>
<SectionAction variant="outline">Members</SectionAction>
</SectionActionLink>
<SectionActionLink to="/">
<SectionActionLink to={`/teams/${team.id}/settings`}>
<SectionAction variant="outline">Settings</SectionAction>
</SectionActionLink>
</SectionActions>

View File

@ -0,0 +1,544 @@
import React from 'react';
import Input from 'shared/components/Input';
import updateApolloCache from 'shared/utils/cache';
import produce from 'immer';
import Button from 'shared/components/Button';
import {
useGetTeamQuery,
RoleCode,
useCreateTeamMemberMutation,
useDeleteTeamMemberMutation,
GetTeamQuery,
GetTeamDocument,
} from 'shared/generated/graphql';
import { UserPlus, Checkmark } from 'shared/icons';
import styled, { css } from 'styled-components/macro';
import { usePopup, Popup } from 'shared/components/PopupMenu';
import TaskAssignee from 'shared/components/TaskAssignee';
import Member from 'shared/components/Member';
const MemberListWrapper = styled.div`
flex: 1 1;
`;
const SearchInput = styled(Input)`
margin: 0;
`;
const UserMember = styled(Member)`
padding: 4px 0;
cursor: pointer;
&:hover {
background: rgba(${props => props.theme.colors.bg.primary}, 0.4);
}
border-radius: 6px;
`;
const TeamMemberList = styled.div`
margin: 8px 0;
`;
type UserManagementPopupProps = {
users: Array<User>;
teamMembers: Array<TaskUser>;
onAddTeamMember: (userID: string) => void;
};
const UserManagementPopup: React.FC<UserManagementPopupProps> = ({ users, teamMembers, onAddTeamMember }) => {
return (
<Popup tab={0} title="Invite a user">
<SearchInput width="100%" variant="alternate" placeholder="Email address or name" name="search" />
<TeamMemberList>
{users
.filter(u => u.id !== teamMembers.find(p => p.id === u.id)?.id)
.map(user => (
<UserMember
key={user.id}
onCardMemberClick={() => onAddTeamMember(user.id)}
showName
member={user}
taskID=""
/>
))}
</TeamMemberList>
</Popup>
);
};
export const RoleCheckmark = styled(Checkmark)`
padding-left: 4px;
`;
const permissions = [
{
code: 'owner',
name: 'Owner',
description:
'Can view, create and edit team projects, and change settings for the team. Will have admin rights on all projects in this team. Can delete the team and all team projects.',
},
{
code: 'admin',
name: 'Admin',
description:
'Can view, create and edit team projects, and change settings for the team. Will have admin rights on all projects in this team.',
},
{ code: 'member', name: 'Member', description: 'Can view, create, and edit team projects, but not change settings.' },
];
export const RoleName = styled.div`
font-size: 14px;
font-weight: 700;
`;
export const RoleDescription = styled.div`
margin-top: 4px;
font-size: 14px;
`;
export const MiniProfileActions = styled.ul`
list-style-type: none;
`;
export const MiniProfileActionWrapper = styled.li``;
export const MiniProfileActionItem = styled.span<{ disabled?: boolean }>`
color: #c2c6dc;
display: block;
font-weight: 400;
padding: 6px 12px;
position: relative;
text-decoration: none;
${props =>
props.disabled
? css`
user-select: none;
pointer-events: none;
color: rgba(${props.theme.colors.text.primary}, 0.4);
`
: css`
cursor: pointer;
&:hover {
background: rgb(115, 103, 240);
}
`}
`;
export const Content = styled.div`
padding: 0 12px 12px;
`;
export const CurrentPermission = styled.span`
margin-left: 4px;
color: rgba(${props => props.theme.colors.text.secondary}, 0.4);
`;
export const Separator = styled.div`
height: 1px;
border-top: 1px solid #414561;
margin: 0.25rem !important;
`;
export const WarningText = styled.span`
display: flex;
color: rgba(${props => props.theme.colors.text.primary}, 0.4);
padding: 6px;
`;
export const DeleteDescription = styled.div`
font-size: 14px;
color: rgba(${props => props.theme.colors.text.primary});
`;
export const RemoveMemberButton = styled(Button)`
margin-top: 16px;
padding: 6px 12px;
width: 100%;
`;
type TeamRoleManagerPopupProps = {
user: TaskUser;
warning?: string | null;
canChangeRole: boolean;
onChangeRole: (roleCode: RoleCode) => void;
onRemoveFromTeam?: () => void;
onChangeTeamOwner?: (userID: string) => void;
};
const TeamRoleManagerPopup: React.FC<TeamRoleManagerPopupProps> = ({
warning,
user,
canChangeRole,
onRemoveFromTeam,
onChangeTeamOwner,
onChangeRole,
}) => {
const { hidePopup, setTab } = usePopup();
return (
<>
<Popup title={null} tab={0}>
<MiniProfileActions>
<MiniProfileActionWrapper>
{onChangeTeamOwner && (
<MiniProfileActionItem
onClick={() => {
setTab(3);
}}
>
Set as team owner...
</MiniProfileActionItem>
)}
{user.role && (
<MiniProfileActionItem
onClick={() => {
setTab(1);
}}
>
Change permissions...
<CurrentPermission>{`(${user.role.name})`}</CurrentPermission>
</MiniProfileActionItem>
)}
{onRemoveFromTeam && (
<MiniProfileActionItem
onClick={() => {
setTab(2);
}}
>
Remove from team...
</MiniProfileActionItem>
)}
</MiniProfileActionWrapper>
</MiniProfileActions>
{warning && (
<>
<Separator />
<WarningText>{warning}</WarningText>
</>
)}
</Popup>
<Popup title="Change Permissions" onClose={() => hidePopup()} tab={1}>
<MiniProfileActions>
<MiniProfileActionWrapper>
{permissions
.filter(p => (user.role && user.role.code === 'owner') || p.code !== 'owner')
.map(perm => (
<MiniProfileActionItem
disabled={user.role && perm.code !== user.role.code && !canChangeRole}
key={perm.code}
onClick={() => {
if (onChangeRole && user.role && perm.code !== user.role.code) {
switch (perm.code) {
case 'owner':
onChangeRole(RoleCode.Owner);
break;
case 'admin':
onChangeRole(RoleCode.Admin);
break;
case 'member':
onChangeRole(RoleCode.Member);
break;
default:
break;
}
hidePopup();
}
}}
>
<RoleName>
{perm.name}
{user.role && perm.code === user.role.code && <RoleCheckmark width={12} height={12} />}
</RoleName>
<RoleDescription>{perm.description}</RoleDescription>
</MiniProfileActionItem>
))}
</MiniProfileActionWrapper>
{user.role && user.role.code === 'owner' && (
<>
<Separator />
<WarningText>You can't change roles because there must be an owner.</WarningText>
</>
)}
</MiniProfileActions>
</Popup>
<Popup title="Remove from Team?" onClose={() => hidePopup()} tab={2}>
<Content>
<DeleteDescription>
The member will be removed from all cards on this project. They will receive a notification.
</DeleteDescription>
<RemoveMemberButton
color="danger"
onClick={() => {
if (onRemoveFromTeam) {
onRemoveFromTeam();
}
}}
>
Remove Member
</RemoveMemberButton>
</Content>
</Popup>
<Popup title="Set as Team Owner?" onClose={() => hidePopup()} tab={3}>
<Content>
<DeleteDescription>
This will change the project owner from you to this user. They will be able to view and edit cards, remove
members, and change all settings for the project. They will also be able to delete the project.
</DeleteDescription>
<RemoveMemberButton
color="warning"
onClick={() => {
if (onChangeTeamOwner) {
onChangeTeamOwner(user.id);
}
}}
>
Set as Project Owner
</RemoveMemberButton>
</Content>
</Popup>
</>
);
};
const MemberItemOptions = styled.div``;
const MemberItemOption = styled(Button)`
padding: 7px 9px;
margin: 4px 0 2px 8px;
float: left;
min-width: 95px;
`;
const MemberList = styled.div`
border-top: 1px solid rgba(${props => props.theme.colors.border});
`;
const MemberListItem = styled.div`
display: flex;
flex-flow: row wrap;
justify-content: space-between;
border-bottom: 1px solid rgba(${props => props.theme.colors.border});
min-height: 40px;
padding: 12px 0 12px 40px;
position: relative;
`;
const MemberListItemDetails = styled.div`
float: left;
flex: 1 0 auto;
padding-left: 8px;
`;
const InviteIcon = styled(UserPlus)`
padding-right: 4px;
`;
const MemberProfile = styled(TaskAssignee)`
position: absolute;
top: 16px;
left: 0;
margin: 0;
`;
const MemberItemName = styled.p`
color: rgba(${props => props.theme.colors.text.secondary});
`;
const MemberItemUsername = styled.p`
color: rgba(${props => props.theme.colors.text.primary});
`;
const MemberListHeader = styled.div`
display: flex;
flex-direction: column;
`;
const ListTitle = styled.h3`
font-size: 18px;
color: rgba(${props => props.theme.colors.text.secondary});
margin-bottom: 12px;
`;
const ListDesc = styled.span`
font-size: 16px;
color: rgba(${props => props.theme.colors.text.primary});
`;
const FilterSearch = styled(Input)`
margin: 0;
`;
const ListActions = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 8px;
margin-bottom: 18px;
`;
const InviteMemberButton = styled(Button)`
padding: 6px 12px;
`;
const FilterTab = styled.div`
max-width: 240px;
flex: 0 0 240px;
margin: 0;
padding-right: 32px;
`;
const FilterTabItems = styled.ul``;
const FilterTabItem = styled.li`
cursor: pointer;
border-radius: 3px;
display: block;
font-weight: 700;
text-decoration: none;
padding: 6px 8px;
color: rgba(${props => props.theme.colors.text.primary});
&:hover {
border-radius: 6px;
background: rgba(${props => props.theme.colors.primary});
color: rgba(${props => props.theme.colors.text.secondary});
}
`;
const FilterTabTitle = styled.h2`
color: #5e6c84;
font-size: 12px;
font-weight: 500;
letter-spacing: 0.04em;
line-height: 16px;
margin-top: 16px;
text-transform: uppercase;
padding: 8px;
margin: 0;
`;
const MemberContainer = styled.div`
margin-top: 45px;
display: flex;
width: 100%;
`;
type MembersProps = {
teamID: string;
};
const Members: React.FC<MembersProps> = ({ teamID }) => {
const { showPopup, hidePopup } = usePopup();
const { loading, data } = useGetTeamQuery({ variables: { teamID } });
const warning =
'You cant leave because you are the only admin. To make another user an admin, click their avatar, select Change permissions, and select Admin.';
const [createTeamMember] = useCreateTeamMemberMutation({
update: (client, response) => {
updateApolloCache<GetTeamQuery>(
client,
GetTeamDocument,
cache =>
produce(cache, draftCache => {
draftCache.findTeam.members.push({ ...response.data.createTeamMember.teamMember });
}),
{ teamID },
);
},
});
const [deleteTeamMember] = useDeleteTeamMemberMutation({
update: (client, response) => {
updateApolloCache<GetTeamQuery>(
client,
GetTeamDocument,
cache =>
produce(cache, draftCache => {
draftCache.findTeam.members = cache.findTeam.members.filter(
member => member.id !== response.data.deleteTeamMember.userID,
);
}),
{ teamID },
);
},
});
if (loading) {
return <span>loading</span>;
}
if (data) {
return (
<MemberContainer>
<FilterTab>
<FilterTabTitle>MEMBERS OF TEAM PROJECTS</FilterTabTitle>
<FilterTabItems>
<FilterTabItem>{`Team Members (${data.findTeam.members.length})`}</FilterTabItem>
<FilterTabItem>Observers</FilterTabItem>
</FilterTabItems>
</FilterTab>
<MemberListWrapper>
<MemberListHeader>
<ListTitle>{`Team Members (${data.findTeam.members.length})`}</ListTitle>
<ListDesc>
Team members can view and join all Team Visible boards and create new boards in the team.
</ListDesc>
<ListActions>
<FilterSearch width="250px" variant="alternate" placeholder="Filter by name" />
<InviteMemberButton
onClick={$target => {
showPopup(
$target,
<UserManagementPopup
users={data.users}
teamMembers={data.findTeam.members}
onAddTeamMember={userID => {
createTeamMember({ variables: { userID, teamID } });
}}
/>,
);
}}
>
<InviteIcon width={16} height={16} />
Invite Team Members
</InviteMemberButton>
</ListActions>
</MemberListHeader>
<MemberList>
{data.findTeam.members.map(member => (
<MemberListItem>
<MemberProfile showRoleIcons size={32} onMemberProfile={() => {}} member={member} />
<MemberListItemDetails>
<MemberItemName>{member.fullName}</MemberItemName>
<MemberItemUsername>{`@${member.username}`}</MemberItemUsername>
</MemberListItemDetails>
<MemberItemOptions>
<MemberItemOption variant="flat">On 2 projects</MemberItemOption>
<MemberItemOption
variant="outline"
onClick={$target => {
showPopup(
$target,
<TeamRoleManagerPopup
user={member}
warning={member.role && member.role.code === 'owner' ? warning : null}
onChangeTeamOwner={
member.role && member.role.code !== 'owner' ? (userID: string) => {} : undefined
}
canChangeRole={member.role && member.role.code !== 'owner'}
onChangeRole={roleCode => {}}
onRemoveFromTeam={
member.role && member.role.code === 'owner'
? undefined
: () => {
deleteTeamMember({ variables: { teamID, userID: member.id } });
hidePopup();
}
}
/>,
);
}}
>
Manage
</MemberItemOption>
</MemberItemOptions>
</MemberListItem>
))}
</MemberList>
</MemberListWrapper>
</MemberContainer>
);
}
return <div>error</div>;
};
export default Members;

View File

@ -0,0 +1,194 @@
import React from 'react';
import styled, { css } from 'styled-components/macro';
import {
useGetTeamQuery,
useDeleteTeamMutation,
GetProjectsDocument,
GetProjectsQuery,
} from 'shared/generated/graphql';
import { Link } from 'react-router-dom';
import Input from 'shared/components/Input';
const FilterSearch = styled(Input)`
margin: 0;
`;
const ProjectsContainer = styled.div`
margin-top: 45px;
display: flex;
width: 100%;
`;
const FilterTab = styled.div`
max-width: 240px;
flex: 0 0 240px;
margin: 0;
padding-right: 32px;
`;
const FilterTabItems = styled.ul``;
const FilterTabItem = styled.li`
cursor: pointer;
border-radius: 3px;
display: block;
font-weight: 700;
text-decoration: none;
padding: 6px 8px;
color: rgba(${props => props.theme.colors.text.primary});
&:hover {
border-radius: 6px;
background: rgba(${props => props.theme.colors.primary});
color: rgba(${props => props.theme.colors.text.secondary});
}
`;
const FilterTabTitle = styled.h2`
color: #5e6c84;
font-size: 12px;
font-weight: 500;
letter-spacing: 0.04em;
line-height: 16px;
margin-top: 16px;
text-transform: uppercase;
padding: 8px;
margin: 0;
`;
const ProjectAddTile = styled.div`
background-color: rgba(${props => props.theme.colors.bg.primary}, 0.4);
background-size: cover;
background-position: 50%;
color: #fff;
line-height: 20px;
padding: 8px;
position: relative;
text-decoration: none;
border-radius: 3px;
display: block;
`;
const ProjectTile = styled(Link)<{ color: string }>`
background-color: ${props => props.color};
background-size: cover;
background-position: 50%;
color: #fff;
line-height: 20px;
padding: 8px;
position: relative;
text-decoration: none;
border-radius: 3px;
display: block;
`;
const ProjectTileFade = styled.div`
background-color: rgba(0, 0, 0, 0.15);
bottom: 0;
left: 0;
position: absolute;
right: 0;
top: 0;
`;
const ProjectListItem = styled.li`
width: 23.5%;
padding: 0;
margin: 0 2% 2% 0;
box-sizing: border-box;
position: relative;
cursor: pointer;
&:hover ${ProjectTileFade} {
background-color: rgba(0, 0, 0, 0.25);
}
`;
const ProjectList = styled.ul`
display: flex;
flex-wrap: wrap;
padding-top: 16px;
& ${ProjectListItem}:nth-of-type(4n) {
margin-right: 0;
}
`;
const ProjectTileDetails = styled.div`
display: flex;
height: 80px;
position: relative;
flex-direction: column;
justify-content: space-between;
`;
const ProjectAddTileDetails = styled.div`
display: flex;
height: 80px;
position: relative;
flex-direction: column;
align-items: center;
justify-content: center;
`;
const ProjectTileName = styled.div<{ centered?: boolean }>`
flex: 0 0 auto;
font-size: 16px;
font-weight: 700;
display: inline-block;
overflow: hidden;
max-height: 40px;
width: 100%;
word-wrap: break-word;
${props => props.centered && 'text-align: center;'}
`;
const ProjectListWrapper = styled.div`
flex: 1 1;
`;
const colors = ['#e362e3', '#7a6ff0', '#37c5ab', '#aa62e3', '#e8384f'];
type TeamProjectsProps = {
teamID: string;
};
const TeamProjects: React.FC<TeamProjectsProps> = ({ teamID }) => {
const { loading, data } = useGetTeamQuery({ variables: { teamID } });
if (loading) {
return <span>loading</span>;
}
if (data) {
return (
<ProjectsContainer>
<FilterTab>
<FilterSearch placeholder="Search for projects..." width="100%" variant="alternate" />
<FilterTabTitle>SORT</FilterTabTitle>
<FilterTabItems>
<FilterTabItem>Most Recently Active</FilterTabItem>
<FilterTabItem>Number of Members</FilterTabItem>
<FilterTabItem>Number of Stars</FilterTabItem>
<FilterTabItem>Alphabetical</FilterTabItem>
</FilterTabItems>
</FilterTab>
<ProjectListWrapper>
<ProjectList>
{data.projects.map((project, idx) => (
<ProjectListItem key={project.id}>
<ProjectTile color={colors[idx % 5]} to={`/projects/${project.id}`}>
<ProjectTileFade />
<ProjectTileDetails>
<ProjectTileName>{project.name}</ProjectTileName>
</ProjectTileDetails>
</ProjectTile>
</ProjectListItem>
))}
</ProjectList>
</ProjectListWrapper>
</ProjectsContainer>
);
}
return <span>error</span>;
};
export default TeamProjects;

View File

@ -0,0 +1,7 @@
import React from 'react';
const TeamSettings = () => {
return <h1>HI!</h1>;
};
export default TeamSettings;

View File

@ -1,115 +1,36 @@
import React, { useState, useContext, useEffect } from 'react';
import styled from 'styled-components/macro';
import styled, { css } from 'styled-components/macro';
import { MENU_TYPES } from 'shared/components/TopNavbar';
import GlobalTopNavbar from 'App/TopNavbar';
import updateApolloCache from 'shared/utils/cache';
import { Route, Switch, useRouteMatch } from 'react-router';
import Members from './Members';
import Projects from './Projects';
import {
useGetTeamQuery,
useDeleteTeamMutation,
GetProjectsDocument,
GetProjectsQuery,
} from 'shared/generated/graphql';
import { useParams, useHistory } from 'react-router';
import { useParams, useHistory, useLocation } from 'react-router';
import { usePopup, Popup } from 'shared/components/PopupMenu';
import { History } from 'history';
import produce from 'immer';
import { TeamSettings, DeleteConfirm, DELETE_INFO } from 'shared/components/ProjectSettings';
import { Link } from 'react-router-dom';
const ProjectAddTile = styled.div`
background-color: rgba(${props => props.theme.colors.bg.primary}, 0.4);
background-size: cover;
background-position: 50%;
color: #fff;
line-height: 20px;
padding: 8px;
position: relative;
text-decoration: none;
border-radius: 3px;
display: block;
`;
const ProjectTile = styled(Link)<{ color: string }>`
background-color: ${props => props.color};
background-size: cover;
background-position: 50%;
color: #fff;
line-height: 20px;
padding: 8px;
position: relative;
text-decoration: none;
border-radius: 3px;
display: block;
`;
const ProjectTileFade = styled.div`
background-color: rgba(0, 0, 0, 0.15);
bottom: 0;
left: 0;
position: absolute;
right: 0;
top: 0;
`;
const ProjectListItem = styled.li`
width: 23.5%;
padding: 0;
margin: 0 2% 2% 0;
box-sizing: border-box;
position: relative;
cursor: pointer;
&:hover ${ProjectTileFade} {
background-color: rgba(0, 0, 0, 0.25);
}
`;
const ProjectList = styled.ul`
const OuterWrapper = styled.div`
display: flex;
flex-wrap: wrap;
& ${ProjectListItem}:nth-of-type(4n) {
margin-right: 0;
}
`;
const ProjectTileDetails = styled.div`
display: flex;
height: 80px;
position: relative;
flex-direction: column;
justify-content: space-between;
`;
const ProjectAddTileDetails = styled.div`
display: flex;
height: 80px;
position: relative;
flex-direction: column;
align-items: center;
justify-content: center;
`;
const ProjectTileName = styled.div<{ centered?: boolean }>`
flex: 0 0 auto;
font-size: 16px;
font-weight: 700;
display: inline-block;
overflow: hidden;
max-height: 40px;
width: 100%;
word-wrap: break-word;
${props => props.centered && 'text-align: center;'}
`;
const Wrapper = styled.div`
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: center;
max-width: 1400px;
width: 100%;
`;
type TeamPopupProps = {
@ -158,22 +79,16 @@ export const TeamPopup: React.FC<TeamPopupProps> = ({ history, name, teamID }) =
);
};
const ProjectsContainer = styled.div`
margin: 40px 16px 0;
width: 100%;
max-width: 825px;
min-width: 288px;
`;
type TeamsRouteProps = {
teamID: string;
};
const colors = ['#e362e3', '#7a6ff0', '#37c5ab', '#aa62e3', '#e8384f'];
const Projects = () => {
const Teams = () => {
const { teamID } = useParams<TeamsRouteProps>();
const history = useHistory();
const { loading, data } = useGetTeamQuery({ variables: { teamID } });
const [currentTab, setCurrentTab] = useState(0);
const match = useRouteMatch();
useEffect(() => {
document.title = 'Citadel | Teams';
}, []);
@ -184,38 +99,39 @@ const Projects = () => {
</>
);
}
if (data) {
return (
<>
<GlobalTopNavbar
menuType={MENU_TYPES.TEAM_MENU}
initialTab={0}
menuType={[
{ name: 'Projects', link: `${match.url}` },
{ name: 'Members', link: `${match.url}/members` },
]}
currentTab={currentTab}
onSetTab={tab => {
setCurrentTab(tab);
}}
popupContent={<TeamPopup history={history} name={data.findTeam.name} teamID={teamID} />}
onSaveProjectName={() => {}}
projectID={null}
name={data.findTeam.name}
/>
<Wrapper>
<ProjectsContainer>
<ProjectList>
{data.projects.map((project, idx) => (
<ProjectListItem key={project.id}>
<ProjectTile color={colors[idx % 5]} to={`/projects/${project.id}`}>
<ProjectTileFade />
<ProjectTileDetails>
<ProjectTileName>{project.name}</ProjectTileName>
</ProjectTileDetails>
</ProjectTile>
</ProjectListItem>
))}
</ProjectList>
</ProjectsContainer>
</Wrapper>
<OuterWrapper>
<Wrapper>
<Switch>
<Route exact path={match.path}>
<Projects teamID={teamID} />
</Route>
<Route path={`${match.path}/members`}>
<Members teamID={teamID} />
</Route>
</Switch>
</Wrapper>
</OuterWrapper>
</>
);
}
return <div>Error!</div>;
};
export default Projects;
export default Teams;

View File

@ -17,12 +17,17 @@ type ContextMenuEvent = {
taskGroupID: string;
};
type Role = {
code: string;
name: string;
};
type User = {
id: string;
fullName: string;
username: string;
email: string;
role: string;
role: Role;
profileIcon: ProfileIcon;
};
@ -30,6 +35,8 @@ type TaskUser = {
id: string;
fullName: string;
profileIcon: ProfileIcon;
username?: string;
role?: Role;
};
type RefreshTokenResponse = {

View File

@ -26,12 +26,13 @@ export const Default = () => {
<Admin
onInviteUser={action('invite user')}
initialTab={1}
onDeleteUser={action('delete user')}
users={[
{
id: '1',
username: 'jordanthedev',
email: 'jordan@jordanthedev.com',
role: 'Admin',
role: { code: 'admin', name: 'Admin' },
fullName: 'Jordan Knott',
profileIcon: {
bgColor: '#fff',

View File

@ -87,62 +87,85 @@ const Header = styled.div`
min-height: 112px;
`;
const ActionButtonsContainer = styled.div`
display: flex;
align-items: center;
`;
const EditUserIcon = styled(Pencil)``;
const EditUserIcon = styled(Pencil)`
margin-right: 8px;
`;
const LockUserIcon = styled(Lock)`
margin-right: 8px;
`;
const LockUserIcon = styled(Lock)``;
const DeleteUserIcon = styled(Trash)``;
const ActionButtons = () => {
type ActionButtonProps = {
onClick: ($target: React.RefObject<HTMLElement>) => void;
};
const ActionButtonWrapper = styled.div`
margin-right: 8px;
cursor: pointer;
display: inline-flex;
`;
const ActionButton: React.FC<ActionButtonProps> = ({ onClick, children }) => {
const $wrapper = useRef<HTMLDivElement>(null);
return (
<>
<EditUserIcon width={16} height={16} />
<LockUserIcon width={16} height={16} />
<DeleteUserIcon width={16} height={16} />
</>
<ActionButtonWrapper onClick={() => onClick($wrapper)} ref={$wrapper}>
{children}
</ActionButtonWrapper>
);
};
const data = {
defaultColDef: {
resizable: true,
sortable: true,
},
columnDefs: [
{
minWidth: 55,
width: 55,
headerCheckboxSelection: true,
checkboxSelection: true,
},
{ minWidth: 210, headerName: 'Username', editable: true, field: 'username' },
{ minWidth: 225, headerName: 'Email', field: 'email' },
{ minWidth: 200, headerName: 'Name', editable: true, field: 'fullName' },
{ minWidth: 200, headerName: 'Role', editable: true, field: 'role' },
{
minWidth: 200,
headerName: 'Actions',
cellRenderer: 'actionButtons',
},
],
frameworkComponents: {
actionButtons: ActionButtons,
},
const ActionButtons = (params: any) => {
return (
<>
<ActionButton onClick={() => {}}>
<EditUserIcon width={16} height={16} />
</ActionButton>
<ActionButton onClick={() => {}}>
<LockUserIcon width={16} height={16} />
</ActionButton>
<ActionButton onClick={$target => params.onDeleteUser($target, params.value)}>
<DeleteUserIcon width={16} height={16} />
</ActionButton>
</>
);
};
type ListTableProps = {
users: Array<User>;
onDeleteUser: ($target: React.RefObject<HTMLElement>, userID: string) => void;
};
const ListTable: React.FC<ListTableProps> = ({ users }) => {
const ListTable: React.FC<ListTableProps> = ({ users, onDeleteUser }) => {
const data = {
defaultColDef: {
resizable: true,
sortable: true,
},
columnDefs: [
{
minWidth: 55,
width: 55,
headerCheckboxSelection: true,
checkboxSelection: true,
},
{ minWidth: 210, headerName: 'Username', editable: true, field: 'username' },
{ minWidth: 225, headerName: 'Email', field: 'email' },
{ minWidth: 200, headerName: 'Name', editable: true, field: 'fullName' },
{ minWidth: 200, headerName: 'Role', editable: true, field: 'roleName' },
{
minWidth: 200,
headerName: 'Actions',
field: 'id',
cellRenderer: 'actionButtons',
cellRendererParams: {
onDeleteUser: (target: any, userID: any) => {
onDeleteUser(target, userID);
},
},
},
],
frameworkComponents: {
actionButtons: ActionButtons,
},
};
return (
<Root>
<div className="ag-theme-material" style={{ height: '296px', width: '100%' }}>
@ -150,7 +173,7 @@ const ListTable: React.FC<ListTableProps> = ({ users }) => {
rowSelection="multiple"
defaultColDef={data.defaultColDef}
columnDefs={data.columnDefs}
rowData={users}
rowData={users.map(u => ({ ...u, roleName: u.role.name }))}
frameworkComponents={data.frameworkComponents}
onFirstDataRendered={params => {
params.api.sizeColumnsToFit();
@ -298,11 +321,12 @@ const NavItem: React.FC<NavItemProps> = ({ active, name, tab, onClick }) => {
type AdminProps = {
initialTab: number;
onAddUser: ($target: React.RefObject<HTMLElement>) => void;
onDeleteUser: ($target: React.RefObject<HTMLElement>, userID: string) => void;
onInviteUser: ($target: React.RefObject<HTMLElement>) => void;
users: Array<User>;
};
const Admin: React.FC<AdminProps> = ({ initialTab, onAddUser, onInviteUser, users }) => {
const Admin: React.FC<AdminProps> = ({ initialTab, onAddUser, onDeleteUser, onInviteUser, users }) => {
const [currentTop, setTop] = useState(initialTab * 48);
const [currentTab, setTab] = useState(initialTab);
const $tabNav = useRef<HTMLDivElement>(null);
@ -339,7 +363,7 @@ const Admin: React.FC<AdminProps> = ({ initialTab, onAddUser, onInviteUser, user
<span style={{ paddingLeft: '5px' }}>Invite member</span>
</InviteUserButton>
</MemberActions>
<ListTable users={users} />
<ListTable onDeleteUser={onDeleteUser} users={users} />
</TabContent>
</TabContentWrapper>
</Container>

View File

@ -3,7 +3,9 @@ import styled, { css } from 'styled-components/macro';
const Text = styled.span<{ fontSize: string }>`
position: relative;
display: inline-block;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
font-size: ${props => props.fontSize};
color: rgba(${props => props.theme.colors.text.secondary});

View File

@ -1,7 +1,7 @@
import React, { useRef } from 'react';
import styled from 'styled-components';
const CardMember = styled.div<{ bgColor: string; ref: any }>`
const CardMember = styled.div<{ bgColor: string }>`
height: 28px;
width: 28px;
float: right;
@ -30,26 +30,41 @@ const CardMemberInitials = styled.div`
type MemberProps = {
onCardMemberClick?: OnCardMemberClick;
taskID: string;
taskID?: string;
member: TaskUser;
showName?: boolean;
className?: string;
};
const Member: React.FC<MemberProps> = ({ onCardMemberClick, taskID, member }) => {
const CardMemberWrapper = styled.div<{ ref: any }>`
display: flex;
align-items: center;
`;
const CardMemberName = styled.span`
font-size: 16px;
padding-left: 8px;
`;
const Member: React.FC<MemberProps> = ({ onCardMemberClick, taskID, member, showName, className }) => {
const $targetRef = useRef<HTMLDivElement>();
return (
<CardMember
<CardMemberWrapper
key={member.id}
ref={$targetRef}
className={className}
onClick={e => {
if (onCardMemberClick) {
e.stopPropagation();
onCardMemberClick($targetRef, taskID, member.id);
onCardMemberClick($targetRef, taskID ?? '', member.id);
}
}}
key={member.id}
bgColor={member.profileIcon.bgColor ?? '#7367F0'}
>
<CardMemberInitials>{member.profileIcon.initials}</CardMemberInitials>
</CardMember>
<CardMember bgColor={member.profileIcon.bgColor ?? '#7367F0'}>
<CardMemberInitials>{member.profileIcon.initials}</CardMemberInitials>
</CardMember>
{showName && <CardMemberName>{member.fullName}</CardMemberName>}
</CardMemberWrapper>
);
};

View File

@ -1,15 +1,22 @@
import styled from 'styled-components';
import styled, { css } from 'styled-components';
import Button from 'shared/components/Button';
import { Checkmark } from 'shared/icons';
export const RoleCheckmark = styled(Checkmark)`
padding-left: 4px;
`;
export const Profile = styled.div`
margin: 8px 0;
min-height: 56px;
position: relative;
`;
export const ProfileIcon = styled.div<{ bgColor: string }>`
export const ProfileIcon = styled.div<{ bgUrl: string | null; bgColor: string }>`
float: left;
margin: 2px;
background-color: ${props => props.bgColor};
background: ${props => (props.bgUrl ? `url(${props.bgUrl})` : props.bgColor)};
background-position: center;
background-size: contain;
border-radius: 25em;
font-size: 16px;
color: #fff;
@ -60,15 +67,66 @@ export const MiniProfileActions = styled.ul`
export const MiniProfileActionWrapper = styled.li``;
export const MiniProfileActionItem = styled.span`
export const MiniProfileActionItem = styled.span<{ disabled?: boolean }>`
color: #c2c6dc;
cursor: pointer;
display: block;
font-weight: 400;
padding: 6px 12px;
position: relative;
text-decoration: none;
&:hover {
background: rgb(115, 103, 240);
}
${props =>
props.disabled
? css`
user-select: none;
pointer-events: none;
color: rgba(${props.theme.colors.text.primary}, 0.4);
`
: css`
cursor: pointer;
&:hover {
background: rgb(115, 103, 240);
}
`}
`;
export const CurrentPermission = styled.span`
margin-left: 4px;
color: rgba(${props => props.theme.colors.text.secondary}, 0.4);
`;
export const Separator = styled.div`
height: 1px;
border-top: 1px solid #414561;
margin: 0.25rem !important;
`;
export const WarningText = styled.span`
display: flex;
color: rgba(${props => props.theme.colors.text.primary}, 0.4);
padding: 6px;
`;
export const DeleteDescription = styled.div`
font-size: 14px;
color: rgba(${props => props.theme.colors.text.primary});
`;
export const RemoveMemberButton = styled(Button)`
margin-top: 16px;
padding: 6px 12px;
width: 100%;
`;
export const Content = styled.div`
padding: 0 12px 12px;
`;
export const RoleName = styled.div`
font-size: 14px;
font-weight: 700;
`;
export const RoleDescription = styled.div`
margin-top: 4px;
font-size: 14px;
`;

View File

@ -1,46 +1,216 @@
import React from 'react';
import { Popup, usePopup } from 'shared/components/PopupMenu';
import { RoleCode } from 'shared/generated/graphql';
import {
RoleCheckmark,
RoleName,
RoleDescription,
Profile,
Content,
DeleteDescription,
RemoveMemberButton,
WarningText,
ProfileIcon,
Separator,
ProfileInfo,
InfoTitle,
InfoUsername,
InfoBio,
CurrentPermission,
MiniProfileActions,
MiniProfileActionWrapper,
MiniProfileActionItem,
} from './Styles';
const permissions = [
{
code: 'owner',
name: 'Owner',
description:
'Can view and edit cards, remove members, and change all settings for the project. Can delete the project.',
},
{
code: 'admin',
name: 'Admin',
description: 'Can view and edit cards, remove members, and change all settings for the project.',
},
{ code: 'member', name: 'Member', description: "Can view and edit cards. Can't change settings." },
{
code: 'observer',
name: 'Observer',
description: "Can view, comment, and vote on cards. Can't move or edit cards or change settings.",
},
];
type MiniProfileProps = {
displayName: string;
username: string;
bio: string;
profileIcon: ProfileIcon | null;
onRemoveFromTask: () => void;
user: TaskUser;
onRemoveFromTask?: () => void;
onChangeRole?: (roleCode: RoleCode) => void;
onRemoveFromBoard?: () => void;
onChangeProjectOwner?: (userID: string) => void;
warning?: string | null;
canChangeRole?: boolean;
};
const MiniProfile: React.FC<MiniProfileProps> = ({ displayName, username, bio, profileIcon, onRemoveFromTask }) => {
const MiniProfile: React.FC<MiniProfileProps> = ({
user,
bio,
canChangeRole,
onChangeProjectOwner,
onRemoveFromTask,
onChangeRole,
onRemoveFromBoard,
warning,
}) => {
const { hidePopup, setTab } = usePopup();
return (
<>
<Profile>
{profileIcon && <ProfileIcon bgColor={profileIcon.bgColor ?? ''}>{profileIcon.initials}</ProfileIcon>}
<ProfileInfo>
<InfoTitle>{displayName}</InfoTitle>
<InfoUsername>{username}</InfoUsername>
<InfoBio>{bio}</InfoBio>
</ProfileInfo>
</Profile>
<MiniProfileActions>
<MiniProfileActionWrapper>
<MiniProfileActionItem
<Popup title={null} onClose={() => hidePopup()} tab={0}>
<Profile>
{user.profileIcon && (
<ProfileIcon bgUrl={user.profileIcon.url ?? null} bgColor={user.profileIcon.bgColor ?? ''}>
{user.profileIcon.url === null && user.profileIcon.initials}
</ProfileIcon>
)}
<ProfileInfo>
<InfoTitle>{user.fullName}</InfoTitle>
<InfoUsername>{`@${user.username}`}</InfoUsername>
<InfoBio>{bio}</InfoBio>
</ProfileInfo>
</Profile>
<MiniProfileActions>
<MiniProfileActionWrapper>
{onRemoveFromTask && (
<MiniProfileActionItem
onClick={() => {
onRemoveFromTask();
}}
>
Remove from card
</MiniProfileActionItem>
)}
{onChangeProjectOwner && (
<MiniProfileActionItem
onClick={() => {
setTab(3);
}}
>
Set as project owner
</MiniProfileActionItem>
)}
{onChangeRole && user.role && (
<MiniProfileActionItem
onClick={() => {
setTab(1);
}}
>
Change permissions...
<CurrentPermission>{`(${user.role.name})`}</CurrentPermission>
</MiniProfileActionItem>
)}
{onRemoveFromBoard && (
<MiniProfileActionItem
onClick={() => {
setTab(2);
}}
>
Remove from board...
</MiniProfileActionItem>
)}
</MiniProfileActionWrapper>
</MiniProfileActions>
{warning && (
<>
<Separator />
<WarningText>{warning}</WarningText>
</>
)}
</Popup>
<Popup title="Change Permissions" onClose={() => hidePopup()} tab={1}>
<MiniProfileActions>
<MiniProfileActionWrapper>
{permissions
.filter(p => (user.role && user.role.code === 'owner') || p.code !== 'owner')
.map(perm => (
<MiniProfileActionItem
disabled={user.role && perm.code !== user.role.code && !canChangeRole}
key={perm.code}
onClick={() => {
if (onChangeRole && user.role && perm.code !== user.role.code) {
switch (perm.code) {
case 'owner':
onChangeRole(RoleCode.Owner);
break;
case 'admin':
onChangeRole(RoleCode.Admin);
break;
case 'member':
onChangeRole(RoleCode.Member);
break;
case 'observer':
onChangeRole(RoleCode.Observer);
break;
default:
break;
}
hidePopup();
}
}}
>
<RoleName>
{perm.name}
{user.role && perm.code === user.role.code && <RoleCheckmark width={12} height={12} />}
</RoleName>
<RoleDescription>{perm.description}</RoleDescription>
</MiniProfileActionItem>
))}
</MiniProfileActionWrapper>
{user.role && user.role.code === 'owner' && (
<>
<Separator />
<WarningText>You can't change roles because there must be an owner.</WarningText>
</>
)}
</MiniProfileActions>
</Popup>
<Popup title="Remove Member?" onClose={() => hidePopup()} tab={2}>
<Content>
<DeleteDescription>
The member will be removed from all cards on this project. They will receive a notification.
</DeleteDescription>
<RemoveMemberButton
color="danger"
onClick={() => {
onRemoveFromTask();
if (onRemoveFromBoard) {
onRemoveFromBoard();
}
}}
>
Remove from card
</MiniProfileActionItem>
</MiniProfileActionWrapper>
</MiniProfileActions>
Remove Member
</RemoveMemberButton>
</Content>
</Popup>
<Popup title="Set as Project Owner?" onClose={() => hidePopup()} tab={3}>
<Content>
<DeleteDescription>
This will change the project owner from you to this user. They will be able to view and edit cards, remove
members, and change all settings for the project. They will also be able to delete the project.
</DeleteDescription>
<RemoveMemberButton
color="warning"
onClick={() => {
if (onChangeProjectOwner) {
onChangeProjectOwner(user.id);
}
}}
>
Set as Project Owner
</RemoveMemberButton>
</Content>
</Popup>
</>
);
};

View File

@ -1,8 +1,11 @@
import React, { useState, useEffect, useRef } from 'react';
import LabelColors from 'shared/constants/labelColors';
import { Checkmark } from 'shared/icons';
import { SaveButton, DeleteButton, LabelBox, EditLabelForm, FieldLabel, FieldName } from './Styles';
import styled from 'styled-components';
const WhiteCheckmark = styled(Checkmark)`
fill: rgba(${props => props.theme.colors.text.secondary});
`;
type Props = {
labelColors: Array<LabelColor>;
label: ProjectLabel | null;
@ -44,7 +47,7 @@ const LabelManager = ({ labelColors, label, onLabelEdit, onLabelDelete }: Props)
setCurrentColor(labelColor);
}}
>
{currentColor && labelColor.id === currentColor.id && <Checkmark width={12} height={12} />}
{currentColor && labelColor.id === currentColor.id && <WhiteCheckmark width={12} height={12} />}
</LabelBox>
))}
</div>

View File

@ -343,9 +343,12 @@ export const MiniProfilePopup = () => {
left={popupData.left}
>
<MiniProfile
displayName="Jordan Knott"
profileIcon={{ url: null, bgColor: '#000', initials: 'JK' }}
username="@jordanthedev"
user={{
id: '1',
fullName: 'Jordan Knott',
username: 'jordanthedev',
profileIcon: { url: null, bgColor: '#000', initials: 'JK' },
}}
bio="Stuff and things"
onRemoveFromTask={action('mini profile')}
/>

View File

@ -78,7 +78,6 @@ export const Content = styled.div`
max-height: 632px;
overflow-x: hidden;
overflow-y: auto;
padding: 0 12px 12px;
`;
export const LabelSearch = styled.input`
box-sizing: border-box;
@ -266,6 +265,9 @@ export const LabelBox = styled.span<{ color: string }>`
display: flex;
align-items: center;
justify-content: center;
&:hover {
opacity: 0.8;
}
`;
export const SaveButton = styled.input`

View File

@ -56,7 +56,8 @@ const QuickCardEditor = ({
top = pos.top;
left = pos.left;
width = pos.width;
if (window.innerHeight - pos.height > height) {
const isFixed = window.innerHeight / 2 < pos.top;
if (isFixed) {
top = window.innerHeight - pos.bottom - saveCardButtonBarHeight;
fixed = true;
}

View File

@ -0,0 +1,126 @@
import React from 'react';
import Select from 'react-select';
import styled from 'styled-components';
import { mixin } from 'shared/utils/styles';
const colourStyles = {
control: (styles: any, data: any) => {
return {
...styles,
backgroundColor: data.isMenuOpen ? mixin.darken('#262c49', 0.15) : '#262c49',
boxShadow: data.menuIsOpen ? 'rgb(115, 103, 240) 0px 0px 0px 1px' : 'none',
borderRadius: '3px',
borderWidth: '1px',
borderStyle: 'solid',
borderImage: 'initial',
borderColor: '#414561',
':hover': {
boxShadow: 'rgb(115, 103, 240) 0px 0px 0px 1px',
borderRadius: '3px',
borderWidth: '1px',
borderStyle: 'solid',
borderImage: 'initial',
borderColor: '#414561',
},
':active': {
boxShadow: 'rgb(115, 103, 240) 0px 0px 0px 1px',
borderRadius: '3px',
borderWidth: '1px',
borderStyle: 'solid',
borderImage: 'initial',
borderColor: 'rgb(115, 103, 240)',
},
};
},
menu: (styles: any) => {
return {
...styles,
backgroundColor: mixin.darken('#262c49', 0.15),
};
},
dropdownIndicator: (styles: any) => ({ ...styles, color: '#c2c6dc', ':hover': { color: '#c2c6dc' } }),
indicatorSeparator: (styles: any) => ({ ...styles, color: '#c2c6dc' }),
option: (styles: any, { data, isDisabled, isFocused, isSelected }: any) => {
return {
...styles,
backgroundColor: isDisabled
? null
: isSelected
? mixin.darken('#262c49', 0.25)
: isFocused
? mixin.darken('#262c49', 0.15)
: null,
color: isDisabled ? '#ccc' : isSelected ? '#fff' : '#c2c6dc',
cursor: isDisabled ? 'not-allowed' : 'default',
':active': {
...styles[':active'],
backgroundColor: !isDisabled && (isSelected ? mixin.darken('#262c49', 0.25) : '#fff'),
},
':hover': {
...styles[':hover'],
backgroundColor: !isDisabled && (isSelected ? 'rgb(115, 103, 240)' : 'rgb(115, 103, 240)'),
},
};
},
placeholder: (styles: any) => ({ ...styles, color: '#c2c6dc' }),
clearIndicator: (styles: any) => ({ ...styles, color: '#c2c6dc', ':hover': { color: '#c2c6dc' } }),
input: (styles: any) => ({
...styles,
color: '#fff',
}),
singleValue: (styles: any) => {
return {
...styles,
color: '#fff',
};
},
};
const InputLabel = styled.span<{ width: string }>`
width: ${props => props.width};
padding-left: 0.7rem;
color: rgba(115, 103, 240);
left: 0;
top: 0;
transition: all 0.2s ease;
position: absolute;
border-radius: 5px;
overflow: hidden;
font-size: 0.85rem;
cursor: text;
font-size: 12px;
user-select: none;
pointer-events: none;
}
`;
const SelectContainer = styled.div`
position: relative;
padding-top: 24px;
`;
type SelectProps = {
label?: string;
onChange: (e: any) => void;
value: any;
options: Array<any>;
};
const SelectElement: React.FC<SelectProps> = ({ onChange, value, options, label }) => {
return (
<SelectContainer>
<Select
onChange={(e: any) => {
onChange(e);
}}
value={value}
styles={colourStyles}
classNamePrefix="teamSelect"
options={options}
/>
{label && <InputLabel width="100%">{label}</InputLabel>}
</SelectContainer>
);
};
export default SelectElement;

View File

@ -14,7 +14,12 @@ export default {
],
},
};
const profile = { url: 'http://localhost:3333/uploads/headshot.png', bgColor: '#000', initials: 'JK' };
const profile = {
id: '1',
fullName: 'Jordan Knott',
username: 'jordanthedev',
profileIcon: { url: 'http://localhost:3333/uploads/headshot.png', bgColor: '#000', initials: 'JK' },
};
export const Default = () => {
return (
<>

View File

@ -228,7 +228,7 @@ const SaveButton = styled(Button)`
type SettingsProps = {
onProfileAvatarChange: () => void;
onProfileAvatarRemove: () => void;
profile: ProfileIcon;
profile: TaskUser;
};
const Settings: React.FC<SettingsProps> = ({ onProfileAvatarRemove, onProfileAvatarChange, profile }) => {
@ -261,11 +261,15 @@ const Settings: React.FC<SettingsProps> = ({ onProfileAvatarRemove, onProfileAva
<AvatarSettings
onProfileAvatarRemove={onProfileAvatarRemove}
onProfileAvatarChange={onProfileAvatarChange}
profile={profile}
profile={profile.profileIcon}
/>
<Input width="100%" label="Name" />
<Input width="100%" label="Initials " />
<Input width="100%" label="Username " />
<Input value={profile.fullName} width="100%" label="Name" />
<Input
value={profile.profileIcon && profile.profileIcon.initials ? profile.profileIcon.initials : ''}
width="100%"
label="Initials "
/>
<Input value={profile.username ?? ''} width="100%" label="Username " />
<Input width="100%" label="Email" />
<Input width="100%" label="Bio" />
<SettingActions>

View File

@ -1,11 +1,28 @@
import React, { useRef } from 'react';
import styled from 'styled-components';
import { DoubleChevronUp, Crown } from 'shared/icons';
export const AdminIcon = styled(DoubleChevronUp)`
bottom: 0;
right: 1px;
position: absolute;
fill: #c377e0;
`;
export const OwnerIcon = styled(Crown)`
bottom: 0;
right: 1px;
position: absolute;
fill: #c8b928;
`;
const TaskDetailAssignee = styled.div`
&:hover {
opacity: 0.8;
}
margin-right: 4px;
cursor: pointer;
margin: 0 0 0 -2px;
border-radius: 50%;
display: flex;
align-items: center;
position: relative;
float: left;
`;
@ -16,24 +33,30 @@ export const Wrapper = styled.div<{ size: number | string; bgColor: string | nul
display: flex;
align-items: center;
justify-content: center;
color: #fff;
color: rgba(${props => (props.backgroundURL ? props.theme.colors.text.primary : '0,0,0')});
background: ${props => (props.backgroundURL ? `url(${props.backgroundURL})` : props.bgColor)};
background-position: center;
background-size: contain;
font-size: 14px;
font-weight: 400;
&:hover {
opacity: 0.8;
}
`;
type TaskAssigneeProps = {
size: number | string;
showRoleIcons?: boolean;
member: TaskUser;
onMemberProfile: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void;
className?: string;
};
const TaskAssignee: React.FC<TaskAssigneeProps> = ({ member, onMemberProfile, size }) => {
const TaskAssignee: React.FC<TaskAssigneeProps> = ({ showRoleIcons, member, onMemberProfile, size, className }) => {
const $memberRef = useRef<HTMLDivElement>(null);
return (
<TaskDetailAssignee
className={className}
ref={$memberRef}
onClick={e => {
e.stopPropagation();
@ -44,6 +67,8 @@ const TaskAssignee: React.FC<TaskAssigneeProps> = ({ member, onMemberProfile, si
<Wrapper backgroundURL={member.profileIcon.url ?? null} bgColor={member.profileIcon.bgColor ?? null} size={size}>
{(!member.profileIcon.url && member.profileIcon.initials) ?? ''}
</Wrapper>
{showRoleIcons && member.role && member.role.code === 'admin' && <AdminIcon width={10} height={10} />}
{showRoleIcons && member.role && member.role.code === 'owner' && <OwnerIcon width={10} height={10} />}
</TaskDetailAssignee>
);
};

View File

@ -179,6 +179,10 @@ export const TaskDetailsMarkdown = styled.div`
margin: 8px 8px 8px 24px;
list-style: disc;
}
p a {
color: rgba(${props => props.theme.colors.primary});
}
`;
export const TaskDetailsControls = styled.div`

View File

@ -3,7 +3,12 @@ import TextareaAutosize from 'react-autosize-textarea';
import { mixin } from 'shared/utils/styles';
import Button from 'shared/components/Button';
import { Citadel } from 'shared/icons';
import { Link } from 'react-router-dom';
import { NavLink, Link } from 'react-router-dom';
import TaskAssignee from 'shared/components/TaskAssignee';
export const ProjectMember = styled(TaskAssignee)<{ zIndex: number }>`
z-index: ${props => props.zIndex};
position: relative;
`;
export const NavbarWrapper = styled.div`
width: 100%;
@ -110,7 +115,7 @@ export const ProjectTabs = styled.div`
max-width: 100%;
`;
export const ProjectTab = styled.span<{ active?: boolean }>`
export const ProjectTab = styled(NavLink)`
font-size: 80%;
color: rgba(${props => props.theme.colors.text.primary});
font-size: 15px;
@ -128,18 +133,19 @@ export const ProjectTab = styled.span<{ active?: boolean }>`
margin-right: 20px;
}
${props =>
props.active
? css`
box-shadow: inset 0 -2px rgba(${props.theme.colors.secondary});
color: rgba(${props.theme.colors.secondary});
`
: css`
&:hover {
box-shadow: inset 0 -2px rgba(${props.theme.colors.text.secondary});
color: rgba(${props.theme.colors.text.secondary});
}
`}
&:hover {
box-shadow: inset 0 -2px rgba(${props => props.theme.colors.text.secondary});
color: rgba(${props => props.theme.colors.text.secondary});
}
&.active {
box-shadow: inset 0 -2px rgba(${props => props.theme.colors.secondary});
color: rgba(${props => props.theme.colors.secondary});
}
&.active:hover {
box-shadow: inset 0 -2px rgba(${props => props.theme.colors.secondary});
color: rgba(${props => props.theme.colors.secondary});
}
`;
export const ProjectName = styled.h1`

View File

@ -37,9 +37,11 @@ export const Default = () => {
bgColor: '#000',
},
}}
onChangeRole={action('change role')}
onNotificationClick={action('notifications click')}
onOpenSettings={action('open settings')}
onDashboardClick={action('open dashboard')}
onRemoveFromBoard={action('remove project')}
onProfileClick={action('profile click')}
/>
</>

View File

@ -4,6 +4,7 @@ import styled from 'styled-components';
import ProfileIcon from 'shared/components/ProfileIcon';
import TaskAssignee from 'shared/components/TaskAssignee';
import { usePopup, Popup } from 'shared/components/PopupMenu';
import { RoleCode } from 'shared/generated/graphql';
import MiniProfile from 'shared/components/MiniProfile';
import {
CitadelLogo,
@ -27,6 +28,7 @@ import {
ProfileNameWrapper,
ProfileNamePrimary,
ProfileNameSecondary,
ProjectMember,
ProjectMembers,
} from './Styles';
import { Link } from 'react-router-dom';
@ -115,8 +117,9 @@ const ProjectHeading: React.FC<ProjectHeadingProps> = ({
);
};
type MenuType = {
[key: number]: string;
export type MenuItem = {
name: string;
link: string;
};
type MenuTypes = {
[key: string]: Array<string>;
@ -128,28 +131,36 @@ export const MENU_TYPES: MenuTypes = {
};
type NavBarProps = {
menuType?: Array<string> | null;
menuType?: Array<MenuItem> | null;
name: string | null;
currentTab?: number;
onSetTab?: (tab: number) => void;
onOpenProjectFinder: ($target: React.RefObject<HTMLElement>) => void;
onChangeProjectOwner?: (userID: string) => void;
onChangeRole?: (userID: string, roleCode: RoleCode) => void;
onFavorite?: () => void;
onProfileClick: ($target: React.RefObject<HTMLElement>) => void;
onTabClick?: (tab: number) => void;
onSaveName?: (name: string) => void;
onNotificationClick: () => void;
onInviteUser?: ($target: React.RefObject<HTMLElement>) => void;
onDashboardClick: () => void;
user: TaskUser | null;
onOpenSettings: ($target: React.RefObject<HTMLElement>) => void;
projectMembers?: Array<TaskUser> | null;
onRemoveFromBoard?: (userID: string) => void;
};
const NavBar: React.FC<NavBarProps> = ({
menuType,
onInviteUser,
onChangeProjectOwner,
currentTab,
onOpenProjectFinder,
onFavorite,
onTabClick,
onSetTab,
onChangeRole,
name,
onRemoveFromBoard,
onSaveName,
onProfileClick,
onNotificationClick,
@ -165,18 +176,44 @@ const NavBar: React.FC<NavBarProps> = ({
};
const { showPopup } = usePopup();
const onMemberProfile = ($targetRef: React.RefObject<HTMLElement>, memberID: string) => {
showPopup(
$targetRef,
<Popup title={null} onClose={() => {}} tab={0}>
const member = projectMembers ? projectMembers.find(u => u.id === memberID) : null;
const warning =
'You cant leave because you are the only admin. To make another user an admin, click their avatar, select “Change permissions…”, and select “Admin”.';
if (member) {
console.log(member);
showPopup(
$targetRef,
<MiniProfile
profileIcon={projectMembers ? projectMembers[0].profileIcon : { url: null, initials: 'JK', bgColor: '#000' }}
displayName="Jordan Knott"
username="@jordanthedev"
bio="None"
onRemoveFromTask={() => {}}
/>
</Popup>,
);
warning={member.role && member.role.code === 'owner' ? warning : null}
onChangeProjectOwner={
member.role && member.role.code !== 'owner'
? (userID: string) => {
if (user && onChangeProjectOwner) {
onChangeProjectOwner(userID);
}
}
: undefined
}
canChangeRole={member.role && member.role.code !== 'owner'}
onChangeRole={roleCode => {
if (onChangeRole) {
onChangeRole(member.id, roleCode);
}
}}
onRemoveFromBoard={
member.role && member.role.code === 'owner'
? undefined
: () => {
if (onRemoveFromBoard) {
onRemoveFromBoard(member.id);
}
}
}
user={member}
bio=""
/>,
);
}
};
return (
@ -196,11 +233,17 @@ const NavBar: React.FC<NavBarProps> = ({
{name && (
<ProjectTabs>
{menuType &&
menuType.map((name, idx) => {
console.log(`${name} : ${idx} === ${currentTab}`);
menuType.map((menu, idx) => {
return (
<ProjectTab key={idx} active={currentTab === idx}>
{name}
<ProjectTab
key={menu.name}
to={menu.link}
exact
onClick={() => {
// TODO
}}
>
{menu.name}
</ProjectTab>
);
})}
@ -215,10 +258,26 @@ const NavBar: React.FC<NavBarProps> = ({
{projectMembers && (
<>
<ProjectMembers>
{projectMembers.map(member => (
<TaskAssignee key={member.id} size={28} member={member} onMemberProfile={onMemberProfile} />
{projectMembers.map((member, idx) => (
<ProjectMember
showRoleIcons
zIndex={projectMembers.length - idx}
key={member.id}
size={28}
member={member}
onMemberProfile={onMemberProfile}
/>
))}
<InviteButton variant="outline">Invite</InviteButton>
<InviteButton
onClick={$target => {
if (onInviteUser) {
onInviteUser($target);
}
}}
variant="outline"
>
Invite
</InviteButton>
</ProjectMembers>
<NavSeparator />
</>

View File

@ -11,4 +11,22 @@ const LabelColors = {
BLACK: '#344563',
};
export const DarkLabelColors = {
RED: '#e8384f',
ORANGE: '#fd612c',
YELLOW_ORANGE: '#fd9a00',
YELLOW: '#eec300',
YELLOW_GREEN: '#a4cf30',
GREEN: '#62d26f',
BLUE_GREEN: '#37c5ab',
AQUA: '#20aaea',
BLUE: '#4186e0',
INDIGO: '#7a6ff0',
PURPLE: '#aa62e3',
MAGENTA: '#e362e3',
HOT_PINK: '#ea4e9d',
PINK: '#fc91ad',
COOL_GRAY: '#8da3a6',
};
export default LabelColors;

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,11 @@ query findProject($projectId: String!) {
members {
id
fullName
username
role {
code
name
}
profileIcon {
url
initials
@ -40,6 +45,21 @@ query findProject($projectId: String!) {
colorHex
name
}
users {
id
email
fullName
username
role {
code
name
}
profileIcon {
url
initials
bgColor
}
}
${TASK_FRAGMENT}
}
`;

View File

@ -0,0 +1,25 @@
import gql from 'graphql-tag';
export const CREATE_PROJECT_MEMBER_MUTATION = gql`
mutation createProjectMember($projectID: UUID!, $userID: UUID!) {
createProjectMember(input: { projectID: $projectID, userID: $userID }) {
ok
member {
id
fullName
profileIcon {
url
initials
bgColor
}
username
role {
code
name
}
}
}
}
`;
export default CREATE_PROJECT_MEMBER_MUTATION;

View File

@ -0,0 +1,15 @@
import gql from 'graphql-tag';
export const DELETE_PROJECT_MEMBER_MUTATION = gql`
mutation deleteProjectMember($projectID: UUID!, $userID: UUID!) {
deleteProjectMember(input: { projectID: $projectID, userID: $userID }) {
ok
member {
id
}
projectID
}
}
`;
export default DELETE_PROJECT_MEMBER_MUTATION;

View File

@ -0,0 +1,25 @@
import gql from 'graphql-tag';
export const SET_PROJECT_OWNER_MUTATION = gql`
mutation setProjectOwner($projectID: UUID!, $ownerID: UUID!) {
setProjectOwner(input: { projectID: $projectID, ownerID: $ownerID }) {
ok
newOwner {
id
role {
code
name
}
}
prevOwner {
id
role {
code
name
}
}
}
}
`;
export default SET_PROJECT_OWNER_MUTATION;

View File

@ -0,0 +1,18 @@
import gql from 'graphql-tag';
export const UPDATE_PROJECT_MEMBER_ROLE_MUTATION = gql`
mutation updateProjectMemberRole($projectID: UUID!, $userID: UUID!, $roleCode: RoleCode!) {
updateProjectMemberRole(input: { projectID: $projectID, userID: $userID, roleCode: $roleCode }) {
ok
member {
id
role {
code
name
}
}
}
}
`;
export default UPDATE_PROJECT_MEMBER_ROLE_MUTATION;

View File

@ -0,0 +1,27 @@
import gql from 'graphql-tag';
export const CREATE_TEAM_MEMBER_MUTATION = gql`
mutation createTeamMember($userID: UUID!, $teamID: UUID!) {
createTeamMember(input: { userID: $userID, teamID: $teamID }) {
team {
id
}
teamMember {
id
username
fullName
role {
code
name
}
profileIcon {
url
initials
bgColor
}
}
}
}
`;
export default CREATE_TEAM_MEMBER_MUTATION;

View File

@ -0,0 +1,12 @@
import gql from 'graphql-tag';
export const DELETE_TEAM_MEMBER_MUTATION = gql`
mutation deleteTeamMember($teamID: UUID!, $userID: UUID!) {
deleteTeamMember(input: { teamID: $teamID, userID: $userID }) {
teamID
userID
}
}
`;
export default DELETE_TEAM_MEMBER_MUTATION;

View File

@ -6,6 +6,20 @@ export const GET_TEAM_QUERY = gql`
id
createdAt
name
members {
id
fullName
username
role {
code
name
}
profileIcon {
url
initials
bgColor
}
}
}
projects(input: { teamID: $teamID }) {
id
@ -15,6 +29,21 @@ export const GET_TEAM_QUERY = gql`
name
}
}
users {
id
email
fullName
username
role {
code
name
}
profileIcon {
url
initials
bgColor
}
}
}
`;

View File

@ -3,13 +3,21 @@ import gql from 'graphql-tag';
export const CREATE_USER_MUTATION = gql`
mutation createUserAccount(
$username: String!
$roleCode: String!
$email: String!
$fullName: String!
$initials: String!
$password: String!
) {
createUserAccount(
input: { username: $username, email: $email, fullName: $fullName, initials: $initials, password: $password }
input: {
roleCode: $roleCode
username: $username
email: $email
fullName: $fullName
initials: $initials
password: $password
}
) {
id
email
@ -21,6 +29,10 @@ export const CREATE_USER_MUTATION = gql`
initials
bgColor
}
role {
code
name
}
}
}
`;

View File

@ -0,0 +1,14 @@
import gql from 'graphql-tag';
export const DELETE_USER_MUTATION = gql`
mutation deleteUserAccount($userID: UUID!) {
deleteUserAccount(input: { userID: $userID }) {
ok
userAccount {
id
}
}
}
`;
export default DELETE_USER_MUTATION;

View File

@ -4,6 +4,10 @@ query users {
email
fullName
username
role {
code
name
}
profileIcon {
url
initials

View File

@ -0,0 +1,12 @@
import React from 'react';
import Icon, { IconProps } from './Icon';
const Crown: React.FC<IconProps> = ({ width = '16px', height = '16px', onClick, className }) => {
return (
<Icon width={width} onClick={onClick} height={height} className={className} viewBox="0 0 640 512">
<path d="M528 448H112c-8.8 0-16 7.2-16 16v32c0 8.8 7.2 16 16 16h416c8.8 0 16-7.2 16-16v-32c0-8.8-7.2-16-16-16zm64-320c-26.5 0-48 21.5-48 48 0 7.1 1.6 13.7 4.4 19.8L476 239.2c-15.4 9.2-35.3 4-44.2-11.6L350.3 85C361 76.2 368 63 368 48c0-26.5-21.5-48-48-48s-48 21.5-48 48c0 15 7 28.2 17.7 37l-81.5 142.6c-8.9 15.6-28.9 20.8-44.2 11.6l-72.3-43.4c2.7-6 4.4-12.7 4.4-19.8 0-26.5-21.5-48-48-48S0 149.5 0 176s21.5 48 48 48c2.6 0 5.2-.4 7.7-.8L128 416h384l72.3-192.8c2.5.4 5.1.8 7.7.8 26.5 0 48-21.5 48-48s-21.5-48-48-48z" />
</Icon>
);
};
export default Crown;

View File

@ -0,0 +1,13 @@
import React from 'react';
import Icon, { IconProps } from './Icon';
const DoubleChevronUp: React.FC<IconProps> = ({ width = '16px', height = '16px', onClick, className }) => {
return (
<Icon width={width} onClick={onClick} height={height} className={className} viewBox="0 0 448 512">
<path d="M240.97 39.176L435.315 233.52c9.373 9.373 9.373 24.57 0 33.941l-22.667 22.667c-9.357 9.357-24.522 9.375-33.901.04L224 136.147 69.254 290.168c-9.379 9.335-24.544 9.317-33.9-.04l-22.668-22.667c-9.373-9.373-9.373-24.569 0-33.94L207.03 39.176c9.372-9.373 24.568-9.373 33.94 0z" />
<path d="M240.97 221.87l194.344 194.344c9.373 9.373 9.373 24.569 0 33.94l-22.667 22.668c-9.357 9.357-24.522 9.375-33.901.04L224 318.842 69.255 472.862c-9.38 9.336-24.544 9.318-33.901-.04l-22.667-22.666c-9.374-9.373-9.374-24.57 0-33.941L207.03 221.872c9.372-9.373 24.568-9.373 33.94-.001z" />
</Icon>
);
};
export default DoubleChevronUp;

View File

@ -0,0 +1,12 @@
import React from 'react';
import Icon, { IconProps } from './Icon';
const UserPlus: React.FC<IconProps> = ({ width = '16px', height = '16px', onClick, className }) => {
return (
<Icon width={width} onClick={onClick} height={height} className={className} viewBox="0 0 640 512">
<path d="M624 208h-64v-64c0-8.8-7.2-16-16-16h-32c-8.8 0-16 7.2-16 16v64h-64c-8.8 0-16 7.2-16 16v32c0 8.8 7.2 16 16 16h64v64c0 8.8 7.2 16 16 16h32c8.8 0 16-7.2 16-16v-64h64c8.8 0 16-7.2 16-16v-32c0-8.8-7.2-16-16-16zm-400 48c70.7 0 128-57.3 128-128S294.7 0 224 0 96 57.3 96 128s57.3 128 128 128zm89.6 32h-16.7c-22.2 10.2-46.9 16-72.9 16s-50.6-5.8-72.9-16h-16.7C60.2 288 0 348.2 0 422.4V464c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48v-41.6c0-74.2-60.2-134.4-134.4-134.4z" />
</Icon>
);
};
export default UserPlus;

View File

@ -2,7 +2,10 @@ import Cross from './Cross';
import Cog from './Cog';
import Sort from './Sort';
import Filter from './Filter';
import DoubleChevronUp from './DoubleChevronUp';
import Crown from './Crown';
import BarChart from './BarChart';
import UserPlus from './UserPlus';
import Trash from './Trash';
import CheckCircle from './CheckCircle';
import Clock from './Clock';
@ -65,5 +68,8 @@ export {
Square,
Filter,
Sort,
DoubleChevronUp,
UserPlus,
Crown,
ToggleOn,
};

File diff suppressed because one or more lines are too long