cleanup: refactor api architecture & add user roles
This commit is contained in:
501
web/report.20200630.191315.2921.0.001.json
Normal file
501
web/report.20200630.191315.2921.0.001.json
Normal 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"
|
||||
]
|
||||
}
|
498
web/report.20200703.192901.99868.0.001.json
Normal file
498
web/report.20200703.192901.99868.0.001.json
Normal 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"
|
||||
]
|
||||
}
|
@ -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,
|
||||
|
@ -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('/');
|
||||
}}
|
||||
|
@ -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();
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
544
web/src/Teams/Members/index.tsx
Normal file
544
web/src/Teams/Members/index.tsx
Normal 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 can’t 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;
|
194
web/src/Teams/Projects/index.tsx
Normal file
194
web/src/Teams/Projects/index.tsx
Normal 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;
|
7
web/src/Teams/Settings/index.tsx
Normal file
7
web/src/Teams/Settings/index.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
const TeamSettings = () => {
|
||||
return <h1>HI!</h1>;
|
||||
};
|
||||
|
||||
export default TeamSettings;
|
@ -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;
|
||||
|
9
web/src/citadel.d.ts
vendored
9
web/src/citadel.d.ts
vendored
@ -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 = {
|
||||
|
@ -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',
|
||||
|
@ -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>
|
||||
|
@ -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});
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
`;
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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')}
|
||||
/>
|
||||
|
@ -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`
|
||||
|
@ -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;
|
||||
}
|
||||
|
126
web/src/shared/components/Select/index.tsx
Normal file
126
web/src/shared/components/Select/index.tsx
Normal 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;
|
@ -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 (
|
||||
<>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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`
|
||||
|
@ -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`
|
||||
|
@ -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')}
|
||||
/>
|
||||
</>
|
||||
|
@ -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 can’t 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 />
|
||||
</>
|
||||
|
@ -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
@ -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}
|
||||
}
|
||||
`;
|
||||
|
25
web/src/shared/graphql/project/createProjectMember.ts
Normal file
25
web/src/shared/graphql/project/createProjectMember.ts
Normal 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;
|
15
web/src/shared/graphql/project/deleteProjectMember.ts
Normal file
15
web/src/shared/graphql/project/deleteProjectMember.ts
Normal 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;
|
25
web/src/shared/graphql/project/setProjectOwner.ts
Normal file
25
web/src/shared/graphql/project/setProjectOwner.ts
Normal 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;
|
18
web/src/shared/graphql/project/updateProjectMemberRole.ts
Normal file
18
web/src/shared/graphql/project/updateProjectMemberRole.ts
Normal 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;
|
27
web/src/shared/graphql/team/createTeamMember.ts
Normal file
27
web/src/shared/graphql/team/createTeamMember.ts
Normal 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;
|
12
web/src/shared/graphql/team/deleteTeamMember.ts
Normal file
12
web/src/shared/graphql/team/deleteTeamMember.ts
Normal 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;
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
14
web/src/shared/graphql/user/deleteUser.ts
Normal file
14
web/src/shared/graphql/user/deleteUser.ts
Normal 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;
|
@ -4,6 +4,10 @@ query users {
|
||||
email
|
||||
fullName
|
||||
username
|
||||
role {
|
||||
code
|
||||
name
|
||||
}
|
||||
profileIcon {
|
||||
url
|
||||
initials
|
||||
|
12
web/src/shared/icons/Crown.tsx
Normal file
12
web/src/shared/icons/Crown.tsx
Normal 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;
|
13
web/src/shared/icons/DoubleChevronUp.tsx
Normal file
13
web/src/shared/icons/DoubleChevronUp.tsx
Normal 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;
|
12
web/src/shared/icons/UserPlus.tsx
Normal file
12
web/src/shared/icons/UserPlus.tsx
Normal 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;
|
@ -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,
|
||||
};
|
||||
|
211
web/src/shared/undraw/Empty.tsx
Normal file
211
web/src/shared/undraw/Empty.tsx
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user