Addition of project Metrics admin page.
This commit is contained in:
parent
244d02b277
commit
24dd787536
@ -1,5 +0,0 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACoxUS2/bOBD+KwLPpkKKFCUK2IPhtFgjTWMk7u5ibxQ5jInIokFRToMi/31BWa6dooe9CaN5fY/hD9SrPaAGLQ+HT0fo44AWKLopVJBCYCIwYVvKGiYaUuY1KwVl5F+0QO4O3lCDKq411RIwAwGYG1LillmNjQFJCTWkKHnqqZ4H1PxAyuUGjk5D7oe/IAzO96hBf7ve+Nc0W7lcd340efAdrPshql6nZb6uV3f4y3KzfdicsgYYUnHuDGqQopSAtgozIBZzpi2uiRS44i2zoEsra3kqc32E0KsuH8zLZb7xsYeomyIvWE5wIdH7AhkVVVq5VQNs3w5pi4mi2xRfTOHps/mBjhBQUyzOZJ7a3ejO3eyHdnSduYkqPEO0Qe3h1YcXOKoOLdAh+AOE6GDi5uEpu6xESU7yQhSEoAW6V3rnesjWt9lDNwHWNeFgGJZgDeaSKiw5Y9iwUlWFsFVVWLRAdxB66K663jsd/OBtzGbKsw9zVr6Prh/9OGTrPsJzUPFU91l1A6AF2oxt54bdIyjztvWPY/rXj13CshpDgD5mGxV32Z9q2KEGSVVrqqSSSgmtLZQELDFECVIZEEaA4YWpDJfcCC6JFly1lBtV0tamCFqghzEexnjm//vVEtvg9nswlw22E8ebTkXrw35toI/OuiTNOWMyVNdNqLK556pz0McPJKMGEQqFYTVgW1CBeW0UViWTWAgmLFAtq4KeCLPuefzJE7HWcGbLuhatbCmBwlY1J5VtidGWaWnKFqqiIlUNtdSEtCWndSs1MUBLbhK64M2o469WYJNAj2OfrjNbJ9ivrsffBb8wsgk+eu27JJjr4OltiLD/nWorvz/4wcWE387Sfhvg3vfp73z/M2dz8dLHSwzSHWSn0wNmVNUybEpTYW5si6WWFhvZKq0Zg7JM3W+9foGQJYMp10+anD318JQtg965CDqOIU3+ZwJ1kvPz+WQufORfP21XPsDycFjM0T+OiaRTs7MBPrwr34aUvvNDvII8Q3uCzp4Xu7KTgePpqXIpKIypwQiKC05LzKWkuJZlhduqbmUraij1xa7ZIxgXQMep3zaMadrM7O98uQzRWaXjkI7ni9eTnWZ/zim/Lnkl28/q0/Ar6WYrzZ0oSZZ9Oj2ck4P+17v5/v7+/h8AAAD//wMA7SBOIigGAAA=
|
||||
@ -1,5 +0,0 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpSUTW/cNhCG/4owZ1HmlyhRN2OdoAs39SJZpEFuFDmKCcvUgqScGIb/e0HtFo5PRS8EMeIM33nmpV4gmEeEAa5Ppw9PGHKCGrLfQpxyRagiVByZGIQamGwo5VqJ7jvU4G/xGQbopLXMaiQCFRLpaEtGMVniHGpGmaO8laWm+ZFgeAHjG4dP3mKzpK8Yk18CDPC3D275We42vrHzsromLjPuQ8om2CLmr/3ulvx5fTjeHc6nEqaS3HgHAxjGKNrJEIF0IlLYifRUK9LJUUxo20n3+pzmQ8YYzNwk9/B2v1tywGwH3nDRUMI1vNbgTDZF8mgSHp9PRcWG6KbE6y28bYcXeMIIA6//hXkud2Vnf/WYxtXP7uq8ZpMe0rZOxuYlPkMNp7icMGaPG5/rlPBxnJ+PJj18PJ8p2/ThF9o1o9sta8gwABM91LABmmeT/RKqi8bd7DHk7WOOPiRv/6uWgBruvlSH2eRpiY/v5vHljHn/Pyh/MvbeB6z2NzAAZcid6JFMnCkie2eIaYUmSgk1IbO64wxq2C0h+7Aua6r2IeOPuLUEA3w0c8Kzvuto731Gm9dY+vymiq8OcXGrzdXbLBltaCMoLUlrPq25+ozOR7QZSxPHuF7qfX2fwhXfknZrjBhydTD5vvrDpHsYQJveMqONNkZZO2FLcaKOGkU7h8opdJK7zkktnZKaWiXNyKQzLRunEoEasFin2tyqZTcq7pDQQk9ORpGxx46MTLuWW2w1ltbeOFZ388bf9lSiE0Tj5IjUzBAthSBOtKbjauo6Pv2G5OIHRgtgh0/nd7cpUM716BQjXLKWSK0Z6XXbkbHrRz2qHltbNN9iDDj/BuqTt3FJy5Sri0Oqd+g+r6H8O6rNLT99IL+2Gd0s9gFjVWZsfChP5TLW19fX138AAAD//wMAZhfta4IEAAA=
|
||||
@ -1,5 +0,0 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpRTy27UMBT9lcjrcepXnDi7agpiVKAjGAFi5/heU6upM3KcAqrm35Ezg6Ds2ETRlY/vefmZRPuIpCfXx+OrJ4x5JhuSwzoSTGjKNGXywGUvdc9VzVjDtGm+kg0Jt/iT9KRVznFnkErUSBWwhg7SOwqAhjMOTDSq3Gm/zaR/JjbUgE/BYT3NnzDNYYqkJ59DhOl72W1D7cZpgTpNI+7inG10hcz73faWvr3eH+7251MzzgVcByA9sZwzdN5SicxTJZ2nHTOatmqQHl3jTWfOsBAzpmjHeoaHP/thyhGz60UtZM2oMOS0IWCzLZQHO+Ph57GwWC26KfPNOl5/+2fyhIn0YvPbzPN1V24MV4/zsIQRrs7fbOeHovKYpiOmHHD1ZJU5jjaHKVaXTdsxYMxkQ+4+VvvRZj+lxxdGfTzr3/2H/EPZ/uoHuiUjbKclZtITrjjZkHfW3YeI1e6G9IRxFCA7pF5wTVUHltpGGqq11B65M60omO0Uc4jLtMzVLmb8llYBpCev7Tjjmfp1cvcho8tLKqq+6NKFfZpgcbn64z9nNaslYwW05OOSqw8IIaHLWPQd0nK57x+I0GIFbZeUMOZqb/N99cbO96QnxnaOW2ONtdo5jw1Dz4BZzVpADRpBCWhBGQVaGea0sgNXYBs++DIhG4Il7mptmDIIojS84aKjqu2AdsJx2gnh7eChsQgvfKzuxjUa1zGFIKlBD1QZbqlRUlKQjW2F9m0r/F+WXNLnrBgM+HR+KysDDdAhaE6F4g1VxnDamaalQ9sNZtAdNq5wvsUUcfzLqHfBpWmefK4u5aleWPdhieW9V2uRvodIf6wZ3UzuAVNVMrYhlnpfYj2dTqdfAAAA//8DALKjFbI2BAAA
|
||||
@ -1,5 +0,0 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpSTTW/cOAyG/4qhs+Xoy7LlWzDZRQdpN4M2aBd7k0W6I9SRBrKctgjmvy/kmaJNb70JlEi+fF7qhQT7hGQgt6fTX88Y8kJqkv0WEkxoyjRl8pHLQeqBq4Zxxg3v/iM18ff4nQykU85xZ5BK1EgVsJaOcnIUAA1nHJhoValpPy9keCHWN4DP3mETl4+YFh8DGcgnHyB+Lb2tb9wcV2hSnHEflmyDK2L+2e/u6dvbw+PD4fJqwaUkNx7IQCznDN1kqUQ2USXdRHtmNO3UKCd07WR6c0nzIWMKdm4W+PKzP8QcMLtBNEI2jApDzjUBm22RPNoFH7+fiooN0V2J11t4Ow4v5BkTGUT9A+al3I2b/Y2LT082wM3kg1+OpCanFE+YsscNxzbhPNvsY6iuTXazx5BJTR4+VIfZ5immp1eMPlxG3//B5O+sO/qA1f6ODIRxFCB7pJPgmqoeLLWtNFRrqSfkznSCk5rsYsg+rHFdqn3I+DltKslA/rbzghd9t8kdfUaX11Sk/6uL1/jN512EEmCkJocUYXW5+ombs4Y1kpXLhzWf1ly9R/AJXcYy02Nar+V/SxFabEm7NSUMuTrYfKze2OVIBmJs77g11lirnZuwZTgxYFazDlCDRlACOlBGgVaGOa3syBXYlo9TiRTdxd1qWyjHOg0cBFUaJFUSWzpaFFSD7KwU3di67hXW6mHe7HA9UwiSGpyAKsMtNUpKCrK1ndBT14npFyRXxzkrvAGfL19jU6ABegTNqVC8pcoYTnvTdnTs+tGMusfWFc33mALOv4B6512KS5xydV2Y6hW692so37valuerD/TbZtlddF8wVcVy60PZ5qvL5/P5/D8AAAD//wMAYTXFVSUEAAA=
|
||||
@ -1,5 +0,0 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACoxU227cNhD9FYGvFRXeRJF6c20HXaSOjeyiLfo2Ioc2a5laUJSTwPC/F9LauaJAXwRhSM45c+bMPJEED0h6cnY8Xj5iKjOpSYlbSDChKdOUyQOXvdQ9Vw1jXdty/TepSXyHn0lPOuUcdxapRI1UedbSQQZHvUfLGfdMtGrNCbcz6Z8IxMbjY3TYTPMfmOc4JdKTP2Py08cVG2LjxmnxTZ5G3KW5QHIrmfe783f097Obw/XN6daM8/q4iZ70BDhn6AJQiSxQJV2ghllNOzXIgK4N1tjTs5gK5gRjM/v7r/h+KgmL60UjZMOosOS5Jh4KrJQHmPHw+biy2CS6WOP1Ft5++yfyiJn0on4V85TujRvjm4d5WOLo32xfUpNjno6YS8RNjet99ZUEZw1rhBaMkZpcgbuLCavdRXU9biU6wxR6SS0GT5XlQK2SknrZQid06DoRSE3eYU44fpP1Kro8zVMo1YvI1Xc4+7PzywTDiCvGWxhnJDXZzfsCycM4Jbz8hG4pp1yHvKzH51MqMS3TMle7VPA2w8v56/vzJWdMpbqBclf9BvMd6YkF4zhYsADauYAtw8A8A806j9pr9Er4ziurvFaWOa1g4MpDy4ewRkhNfl013C/O4Tx/ZbOZZBw3DtVLn87HiKl8JyPpCeMovDRIg+CaKuOBQist1VrqgNzZTvBXmAPkWyykJ8EMw8CcG4Sy3IGwIH1g0utgheg6UEG2nRJat6wD57tOcOVlhw5kkMwIUpObPPnFlR9bLbcG3OTpH3RllYr0RLO2NQ6GQWjFDViDimtltMXOqgBMSTcAbw0MA2fQMmeNcMC1BTX4NnxR6TLdxoQXcT6O8PkbXNPoRv7iAAwPoH64/jbDA36c8v37k42b95cHUpMPS1o3QrVbLfIxJvpJrxN9tYwlHu4ygkd/NXn82Ue4jku1TWhgQYNTjkouBFVBKzqA5NRytNYa2XHRkZpcTO4ec7U6DGJap+pLsut9dZbdXSzoypJXfn9tPK731c0IJUz54btN8k1hPwrQCMnZ2hmPj6dttHHU3hv0mlOheEuVtZwa23Z06MxgB22wdati10s5LqX6gD5mdGWr+MWLG+b5Hbr7n8V4dcGLRTlbvbY/bbFN2v+1xJ5r8oAwLxkftm3dP5FdSpg35IvlNIq7dBXHMc7opuRn0pvW8EYbLV4Y/uc9LXWjhTXPz8/P/wIAAP//AwDpyqsjGwYAAA==
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACoxUS2/bOBD+KwLPpkJRFCUK2IPhtFgjTWMk7u5ibxQ5jInIpEFRToMi/31BWa6dooe9CaN5fY/hD+TkHlCLlofDpyO4OKAFinYKUUI5JhyTcluwtqJt1eSiFpyU7F+0QPYO3lCLaqZUoQTgEjhgpkmFu9IorDWIghSa0IqlnvJ5QO0PJG2u4WgV5H74C8JgvUMt+ts67V/TbGlz1ftR58H3sHZDlE6lZb6uV3f4y3KzfdicsgYYUnFuNWqRrlmheSOwMlRgxiTBHaEGC6U1F6STpjOnMusiBCf7fNAvl/naRwdRtTSnZU4wFeh9gbSMMq3cyQG2b4e0xUTRbYovpvD02f5ARwiopYszmad2N6q3N/uhG22vb6IMzxBNkHt49eEFjrJHC3QI/gAhWpi4eXjKLisVJCc55ZQQtED3Uu2sg2x9mz30CbBUDWGgSyzAaMxEIbFgZYl1WcmaclPXNAG+g+Cgv+p6b1XwgzcxmynPPsxZeRetG/04ZGsX4TnIeKr7LPsB0AJtxq63w+4RpH7b+scx/XNjn7CsxhDAxWwj4y77Uw471CIhG1VIIYWUXCkDFQFDNJGc1Bq45qAZ1bVmgmnOBFGcya5gWlZFZ1IELdDDGA9jPPP//WqJbbD7PejLBtuJ400vo/Fhv9bgojU2SXPOmAzV9xOqbO656i24+IFk1CJSANVlA9jQgmPWaIllVQrMeckNFErUtDgRZuzz+JMnYoxmpamahneiKwhQUzeM1KYjWplSCV11UNOa1A00QhHSVaxoOqGIhqJiOqELXo8q/mqFchLocXTpOrN1gv1qHf7O2YWRTfDRK98nwWwPT29DhP3vVFv5/cEPNib8Zpb22wD33qW/8/3PnM3FSx8vMUh3kE2nR6uOUMI4po0ymOma4g4kwaKsGK8bIAAKLdCtVy8QsmQwad2kydlTD0/ZMqidjaDiGNLkfyZQJzk/n0/mwkf+9dN25QMsD4fFHP3jmEg6NTsb4MO78m1I6Ts/xCvIM7Qn6M15sSs7aTieniqbglzrBjQvMGVFhZkQBW5EVeOubjrR8QYqdbFr9gjaBlBx6rcNY5o2M/s7Xy5DtEaqOKTj+eLVZKfZn3PKr0teyfaz+jT8SrrZSnOngiTLPp0ezslB/+vdfH9/f/8PAAD//wMABJt80ygGAAA=
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpSUwW7cNhCGX0WYsyiTEkWJuhnrBF24qRfJIi16ozijmLBMLUjKiWHsuxfUbpD6VPRCECPO8J9vfuoNvHkmGOD2dPrwQj5FKCG5LVTzWjGuGG+OQg5tM/C+kk0tRdv/DSW4e3qFATpprbCaWEOKmETesrGZLEMkLbhAXrcy1zTfIgxvYFyF9OIsVUv8SiG6xcMAfzqPy/d8t3GVnZcVq7DMtPcxGW+zmD/2u3v2++3h+HC4nIoUc3LlEAbATgpUvWZ2qjWT0nA28npi2iIqzUczjdMlzflEwZu5ivj0635ckqdkh7qqm4qzWsO5BDTJZMmjiXR8PWUVG6K7HC+38LYd3uCFAgx1+RPmpdyNnd3NcxxXN+PNZU0mPsVtnYxNS3iFEk5hOVFIjjY+tzHS8zi/Hk18+ng5k7fxww+yayLcLatPMIBoOihhAzTPJrnFF1eNu9mRT9vHFJyPzv5XrQZKePhSHGaTpiU8v5vHlwvm/f+g/MnYR+ep2N/BAFxQjU1PbKqFYrJHw0zbaKZUoyYSVne1gBJ2i0/Or8sai71P9C1sLcEAH80c6aLvNthHl8imNeQ+/1LZV4ew4GpT8WuWgle8ajjPSWs6ran4TOgC2US5iWNYr/W+vk+pVb0l7dYQyKfiYNJj8ZuJjzCANr0VRhttjLJ2opbTxJEbxTskhYpQ1tih1BKV1NwqaUYh0bRinHIESqBsnWJz69hMulNSsNaaiUkpiBmFxPpOj42Smqwc33EsHuacZ2zPJWHDNE3IpBaGadk0DJvWdLWauq7O/H8iufpB8AwY6eXy7jYFCrEnVILl18yk1oL1uu3Y2PWjHlVPrc2a7yl4mv8F6pOzYYnLlIqrQ4p36D6vPv87is0t351nP7YZ3S32iUKRZ2ycz0/lOtbz+Xz+BwAA//8DAK3VVNOCBAAA
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpSTQW/cNhCF/4owZ1EmKYoSdTPWKbpwEy+SRVr0RnFGMWGZWlCUk8DY/x5Qu4GT3noRhAEf571vhq8Q7DNBD7en07sXCmmBEpLfSpJLzbhmvD4K1Td1z7tK1Uoa3v4LJfh7+g49tMo54QyxmjQxhbxhQz06hkhGcIFcNirfab8s0L+C9RXSi3dUzctnioufA/Twtw84f829ra/cNK9YxXmifViSDS6b+bDf3bO/bg/Hh8Pl1EJLFlceoQdslUDdGeZGaZhSlrOBy5EZh6gNH+w4jBeZD4lisFO14NNbf5xToOR6Wcm64kwaOJeANtlsebALHb+fsosN0V2ul1t5++1f4YUi9LL8CfNy3Y2b/M3zMqx+wpvLN9nlKac8xflEMXnamGwxp8kmP4fi2mk3eQoJSnj4VBwmm8Y5Pv8G6tMl//5/xD/m7u++kVsT4W5eQ4IehOJQwnvrHn2gYn8HPXBBEuuO2CiFZqpDy2xTG6Z1rUcSzrRSQAm7OSQf1nldin1I9CVuAaCHP+y00MX6bXSPPpFLa8yp/tF5Fw5xxtWl4o2/4BWvap6dPKzptKbiI6GP5BLlfMe4Xu/7j0RquYl2a4wUUnGw6bH40y6P0IOxnRPWWGOtdm6khtPIkVvNWySNmlBJbFEZhVoZ7rSyg1BoGzGMuQIlUB53sW2YwIGjVophbTRTQ9My46xkrtV80AYFbYjfOBYPU9ZZ13FFWDNDIzJlhGVG1TXDurGt1GPbyqz7ieQ6fcEzYKSXy1vZHGjEjlALJpVomDJGsM40LRvabjCD7qhx2fM9xUDTL6DeexfnZR5TcV2e4jd0H9eQ33uxLdJXH9i3bUZ3s3uiWOQZWx/yel/Hej6fzz8AAAD//wMAtGYnyDYEAAA=
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpRTwY7cKBD9FYuz8QDG2Pg26tlVWpPstJJRstobpoo0igdaGE8SjfrfV7g7Sia33NCDV/XqveKFBPOEZCS3p9NfzxjyQmqS/QYJJhRlirL2kcuxa0c2NLLVnZTyP1ITf4/fyUh6aS23GmmLCqkE1tGpdZYCoOaMAxOdLDXN54WML8T4BvDZW2zi8hHT4mMgI/nkA8SvpbfxjZ3jCk2KM+7Dkk2wRcw/+909fXt7eHw4XF4tuBRy44GMBHrJQQ2aWic0ldIwOjHhqLYASrPJuMldaD5kTMHMzQJffvaHmANmO4pGtA2jQpNzTcBkUyRPZsHH76eiYrPoruD1Bm/H8YU8YyKjqH+YeSl3Y2d/Y+PTkwlw43zwy5HU5JTiCVP2uNmxTTjPJvsYqmuT3ewxZFKThw/VYTbZxfT0yqMPl9H3fzD5O2OPPmC1vyMjYRwFtANSJ7iicgBDTddqqlSrHHKre8FJTXYxZB/WuC7VPmT8nDaVZCR/m3nBi77bZI8+o81rKtL/VSVr/ObzLkIBGKnJIUVYba5+2s1Zw5qWlcuHNZ/WXL1H8AltxjLTY1qv5X+jCCU20m5NCUOuDiYfqzdmOZKRaDNYbrTRxihrHXYMHQNmFOsBFSgEKaAHqSUoqZlV0kxcgun45ApSdJd0q22hBqE4KieoGGRLpRWc6nZCOvUd4tA5KUT3ytbqYS48YwcmEVqq0QGVmhuqZdtSaDvTC+X6XpQ4flhyTZyz4jfg8+VrbAoUwICgOBWSd1Rqzemgu55O/TDpSQ3Y2aL5HlPA+Rej3nmb4hJdrq4LU72y7v0ayveutuX56gP9tkV2F+0XTFWJ3PhQtvma8vl8Pv8PAAD//wMAZYD86yUEAAA=
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACoxUXW/sJhD9KxavXRzAGBu/pZtcdZUmWd1dtVXfBhgnNA5eYZzcqyj/vcKb3HxcVeqLZQ0w58yZM/NEAtwj6cjp4XD+gCFNZEWSX0KCCUWZoqzac9nVVcfaUlZKK9X8TVbEX+B30pFGWsutRlqhQiodq6mpekudQ80Zd0zUMueEm4l0TwR86fDBWyzH6Q+Mkx8D6cifPrjxMWODL+0wzq6M44CbMCUINpO52qwv6O+n2/319nhrwik/Lr0jHXGN5E61mtpeaColMGqY6Km2zinNDPSmPz7zIWEMMJSTu3vDd2MKmGwnSlGVjApNnlfEQYJM2cCE+++HzGKR6CzHV0t4+e2eyANG0onVq5jHdCd28Cf3k5n94E6WL1mRQxwPGJPHRY3rXfFGgrOSlUIJxsiKXIK99QGLzVlxPeQSwbZMoquoxt5RqTlQLauKuqqGRqi+aUQu8QJjwOFd1ktv4ziNfSpeRC4+4OxO1+cBzIAZ4wsME5IV2Uy7BMHBMAY8/4Z2Tsdc+zjn4/UYkg/zOE/FJiS8ifBy/vp+PceIIRVbSLfFbzDdko5oaC0HDRpAWdtjzbBnjoFijUPlFDopXOOklk5JzaySYLh0UHPT5whZkV+zhrvZWpymNzaLSYZh4VC89Gk9eAzpg4ykI4yjcFWLtBdcUdk6oFBXmipVqR651Y3grzB7iDeYSEf61hjDrDVCam5BaKhczyqnei1E04Dsq7qRQqmaNWBd0wguXdWghaqvWCvIimzj6GabPre6WhqwjeM/aFOWinREsbpuLRgjlOQt6BYlV7JVGhste2CysgZ43YIxnEHNrG6FBa40SOPq/odK5+HGBzzz02GA7+9w21KV1S8WoOU9yE/Xv0S4x8cx3l0dbVxene/JinydQ94IxSZb5NEH+k3lib6ch+T3txHBobscHf7sI8zjUiwTKpVrNUpOeyF6KoWoKZjWUGF45ZwxTOvM5my0dxiL7DDwIU/Vj2TXu+I02luf0KY5Zn5/LTyud8V2gNSP8f7DJnlX2GcBSlFxljvj8OG4jRaOyrkWneJUSF5TqTWnra4baprWaKNarG3meD2nw5yKr+h8RJuWil+8uGCub9He/SzGqwteLMpZ9truuMUWaf/XEntekXuEaY54v2zr7olsQsC4IJ/Nx1HchEs/DH5COwY3kY4zplmpdatfKP73Ra5ZqVQjn5+fn/8FAAD//wMAY5ugbB0GAAA=
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACoxUTW/cNhD9KwKvXcr8EkXq5toOunAdG9lFW/Q2JEc2a5laSJSTwPB/L6i1EztBgV4EYciZ9/jmzTyRBA9IOnJ6OFw8Ysoz2ZAc15BgQlOmKZN7rjpmu0bXlgthW/432ZB4iV9JR1rlPfcWqUSNVAXWUCd7T0NAyxkPTDSq1ITbmXRPBGId8DF6rMf5D5zmOCbSkT9jCuPngg2x9sO4hHoaB9ymOUPyhczH7dkl/f30Zn99c7w141yS6xhIR4J0lmHwVBjtqQLmqHGM0RCM74VAaFpxTIsp45RgqOdw/x0/jDlh9p2ohawZFZY8b0iADIWygxn3Xw+FxSrReYlv1vD62z2RR5xIJzavYh7LnfghnjzMbolDOFm/ZEMO03jAKUdc1bjeVd9JcFazWmjBGNmQK/B3MWG1Pa+uh/JE8IYpDJJa7ANVlgO1SkoaZAOt0H3bip5syCVOCYc3Va+in8Z57HP1InL1Dmd3enaRwA1YMD7AMCPZkO28y5ACDGPCiy/ol3ystZ+Wcnw2phzTMi5ztU0Zbyd4OX/NP1umCVOubiDfVb/BfEc6YsF4DhYsgPa+x4ZhzwIDzdqAOmgMSoQ2KKuCVpZ5rcBxFaDhri8RsiG/Fg13i/c4z9/ZrCYZhpVD9dKnsyFiyu9kJB1hHEWQBmkvuKbKBKDQSEu1lrpH7m0r+CvMHqZbzKQjvXHOMe+dUJZ7EBZk6JkMurdCtC2oXjatElo3rAUf2lZwFWSLHmQvmSm2u5nGsPj8Y6vl2oCbafwHfS5SkY5o1jTGg3NCK27AGlRcK6Mttlb1wJT0DnhjwDnOoGHeGuGBawvKhab/ptJFuo0Jz+N8GODrG1xT61r+4gEM70H9cP3DBA/4eZzuPx5tXH+82JMN+bSkshGqbbHI55joF10m+moZctzfTQgBw9UY8GcfYRmXap3Q1lomnHFUCtNTJVpNnVGBOmwbqaSRvS1SnY/+HqeqOAxiKlP1rdj1rjqd/F3M6PMyFX5/rTyud9XNALkfp4d3m+TNw34UoBaSswIX8PG4jVaOOgSDQXMqFG+ospZTY5uWutY467TBxhfFrpd8WHL1CUOc0Of1xS9eXDHP7tDf/yzGqwteLMpZ8druuMVWaf/XEnvekAeEeZnwYd3W3RPZpoTTiny+HEdxm67iMMQZ/ZjCTDrOhapN06oXhv99z7DaWCmen5+f/wUAAP//AwAsxW2tGwYAAA==
|
||||
Binary file not shown.
89
PlotLine/Controllers/ProjectMetricsController.cs
Normal file
89
PlotLine/Controllers/ProjectMetricsController.cs
Normal file
@ -0,0 +1,89 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using PlotLine.Services;
|
||||
using PlotLine.ViewModels;
|
||||
|
||||
namespace PlotLine.Controllers;
|
||||
|
||||
public sealed class ProjectMetricsController(ISceneMetricTypeService metrics) : Controller
|
||||
{
|
||||
public async Task<IActionResult> Index(int projectId)
|
||||
{
|
||||
var model = await metrics.GetAsync(projectId);
|
||||
return model is null ? NotFound() : View(model);
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Edit(int projectId, int id)
|
||||
{
|
||||
var model = await metrics.GetEditAsync(projectId, id);
|
||||
return model is null ? NotFound() : View(model);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Save(SceneMetricTypeEditViewModel model)
|
||||
{
|
||||
if (model.MinValue >= model.MaxValue)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.MaxValue), "Maximum value must be greater than minimum value.");
|
||||
}
|
||||
|
||||
if (model.DefaultValue < model.MinValue || model.DefaultValue > model.MaxValue)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.DefaultValue), "Default value must sit inside the metric range.");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return model.MetricTypeID == 0
|
||||
? RedirectToAction(nameof(Index), new { projectId = model.ProjectID })
|
||||
: View("Edit", model);
|
||||
}
|
||||
|
||||
await metrics.SaveAsync(model);
|
||||
TempData["ProjectMetricMessage"] = model.MetricTypeID == 0 ? "Scene metric added." : "Scene metric updated.";
|
||||
return RedirectToAction(nameof(Index), new { projectId = model.ProjectID });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> AddMissingDefaults(int projectId)
|
||||
{
|
||||
await metrics.AddMissingDefaultsAsync(projectId);
|
||||
TempData["ProjectMetricMessage"] = "Missing default scene metrics added.";
|
||||
return RedirectToAction(nameof(Index), new { projectId });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> SetActive(int projectId, int id, bool isActive)
|
||||
{
|
||||
var updated = await metrics.SetActiveAsync(projectId, id, isActive);
|
||||
TempData["ProjectMetricMessage"] = updated
|
||||
? (isActive ? "Metric reactivated." : "Metric deactivated.")
|
||||
: (isActive ? "Metric could not be reactivated." : "Metric could not be deactivated.");
|
||||
return RedirectToAction(nameof(Index), new { projectId });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Move(int projectId, int id, string direction)
|
||||
{
|
||||
direction = string.Equals(direction, "Up", StringComparison.OrdinalIgnoreCase) ? "Up" : "Down";
|
||||
var moved = await metrics.MoveAsync(projectId, id, direction);
|
||||
TempData["ProjectMetricMessage"] = moved
|
||||
? $"Metric moved {direction.ToLowerInvariant()}."
|
||||
: $"Metric could not be moved {direction.ToLowerInvariant()} because it is already at the edge of its group.";
|
||||
return RedirectToAction(nameof(Index), new { projectId });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Remove(int projectId, int id)
|
||||
{
|
||||
var removed = await metrics.RemoveUnusedAsync(projectId, id);
|
||||
TempData["ProjectMetricMessage"] = removed
|
||||
? "Unused metric removed."
|
||||
: "Metric cannot be removed because scene values exist or the metric is still active.";
|
||||
return RedirectToAction(nameof(Index), new { projectId });
|
||||
}
|
||||
}
|
||||
@ -72,6 +72,18 @@ public interface ITimelineSettingsRepository
|
||||
Task SaveAsync(ProjectTimelineSettings settings, IEnumerable<int> metricTypeIds);
|
||||
}
|
||||
|
||||
public interface ISceneMetricTypeRepository
|
||||
{
|
||||
Task<IReadOnlyList<SceneMetricType>> ListForManagementAsync(int projectId);
|
||||
Task<SceneMetricType?> GetAsync(int metricTypeId, int projectId);
|
||||
Task<int> SaveAsync(SceneMetricType metric);
|
||||
Task<bool> SetActiveAsync(int metricTypeId, int projectId, bool isActive);
|
||||
Task<bool> MoveAsync(int metricTypeId, int projectId, string direction);
|
||||
Task AddMissingDefaultsAsync(int projectId);
|
||||
Task<bool> RemoveUnusedAsync(int metricTypeId, int projectId);
|
||||
Task<bool> HasProjectMetricsAsync(int projectId);
|
||||
}
|
||||
|
||||
public interface IPlotRepository
|
||||
{
|
||||
Task<PlotLookupData> GetLookupsAsync();
|
||||
@ -1465,6 +1477,87 @@ public sealed class TimelineSettingsRepository(ISqlConnectionFactory connectionF
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class SceneMetricTypeRepository(ISqlConnectionFactory connectionFactory) : ISceneMetricTypeRepository
|
||||
{
|
||||
public async Task<IReadOnlyList<SceneMetricType>> ListForManagementAsync(int projectId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
var rows = await connection.QueryAsync<SceneMetricType>(
|
||||
"dbo.SceneMetricType_ListForProjectManagement",
|
||||
new { ProjectID = projectId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
return rows.ToList();
|
||||
}
|
||||
|
||||
public async Task<SceneMetricType?> GetAsync(int metricTypeId, int projectId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleOrDefaultAsync<SceneMetricType>(
|
||||
"dbo.SceneMetricType_GetForProject",
|
||||
new { MetricTypeID = metricTypeId, ProjectID = projectId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<int> SaveAsync(SceneMetricType metric)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleAsync<int>(
|
||||
"dbo.SceneMetricType_SaveForProject",
|
||||
new
|
||||
{
|
||||
metric.MetricTypeID,
|
||||
metric.ProjectID,
|
||||
metric.MetricName,
|
||||
metric.Description,
|
||||
metric.MinValue,
|
||||
metric.MaxValue,
|
||||
metric.DefaultValue,
|
||||
metric.SortOrder,
|
||||
metric.IsActive
|
||||
},
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<bool> SetActiveAsync(int metricTypeId, int projectId, bool isActive)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleAsync<bool>(
|
||||
"dbo.SceneMetricType_SetActiveForProject",
|
||||
new { MetricTypeID = metricTypeId, ProjectID = projectId, IsActive = isActive },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<bool> MoveAsync(int metricTypeId, int projectId, string direction)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleAsync<bool>(
|
||||
"dbo.SceneMetricType_MoveForProject",
|
||||
new { MetricTypeID = metricTypeId, ProjectID = projectId, Direction = direction },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task AddMissingDefaultsAsync(int projectId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
await connection.ExecuteAsync("dbo.SceneMetricType_AddMissingDefaultsForProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<bool> RemoveUnusedAsync(int metricTypeId, int projectId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleAsync<bool>(
|
||||
"dbo.SceneMetricType_RemoveUnusedForProject",
|
||||
new { MetricTypeID = metricTypeId, ProjectID = projectId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<bool> HasProjectMetricsAsync(int projectId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleAsync<bool>("dbo.SceneMetricType_ProjectHasMetrics", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TimelinePresetRepository(ISqlConnectionFactory connectionFactory) : ITimelinePresetRepository
|
||||
{
|
||||
public async Task<IReadOnlyList<TimelineViewPreset>> ListAsync(int projectId, int? bookId)
|
||||
|
||||
@ -152,6 +152,8 @@ public sealed class SceneMetricType
|
||||
public int DefaultValue { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public bool IsEnabledForProject { get; set; }
|
||||
public int ValueCount { get; set; }
|
||||
}
|
||||
|
||||
public sealed class SceneMetricValue
|
||||
|
||||
@ -26,6 +26,7 @@ public class Program
|
||||
builder.Services.AddScoped<ITimelineRepository, TimelineRepository>();
|
||||
builder.Services.AddScoped<ITimelinePresetRepository, TimelinePresetRepository>();
|
||||
builder.Services.AddScoped<ITimelineSettingsRepository, TimelineSettingsRepository>();
|
||||
builder.Services.AddScoped<ISceneMetricTypeRepository, SceneMetricTypeRepository>();
|
||||
builder.Services.AddScoped<IPlotRepository, PlotRepository>();
|
||||
builder.Services.AddScoped<IAssetRepository, AssetRepository>();
|
||||
builder.Services.AddScoped<ICharacterRepository, CharacterRepository>();
|
||||
@ -46,6 +47,7 @@ public class Program
|
||||
builder.Services.AddScoped<IChapterService, ChapterService>();
|
||||
builder.Services.AddScoped<ISceneService, SceneService>();
|
||||
builder.Services.AddScoped<ITimelineService, TimelineService>();
|
||||
builder.Services.AddScoped<ISceneMetricTypeService, SceneMetricTypeService>();
|
||||
builder.Services.AddScoped<IPlotService, PlotService>();
|
||||
builder.Services.AddScoped<IAssetService, AssetService>();
|
||||
builder.Services.AddScoped<ICharacterService, CharacterService>();
|
||||
|
||||
@ -150,6 +150,17 @@ public interface ITimelineService
|
||||
Task AddLocationRelationshipByDropAsync(TimelineLocationRelationshipDropViewModel model);
|
||||
}
|
||||
|
||||
public interface ISceneMetricTypeService
|
||||
{
|
||||
Task<ProjectSceneMetricsViewModel?> GetAsync(int projectId);
|
||||
Task<SceneMetricTypeEditViewModel?> GetEditAsync(int projectId, int metricTypeId);
|
||||
Task<int> SaveAsync(SceneMetricTypeEditViewModel model);
|
||||
Task<bool> SetActiveAsync(int projectId, int metricTypeId, bool isActive);
|
||||
Task<bool> MoveAsync(int projectId, int metricTypeId, string direction);
|
||||
Task AddMissingDefaultsAsync(int projectId);
|
||||
Task<bool> RemoveUnusedAsync(int projectId, int metricTypeId);
|
||||
}
|
||||
|
||||
public interface IAnalyticsService
|
||||
{
|
||||
Task<AnalyticsViewModel?> GetStoryHealthAsync(int projectId, int? bookId, decimal? startChapterNumber, decimal? endChapterNumber, decimal? startSceneNumber, decimal? endSceneNumber);
|
||||
@ -1954,6 +1965,63 @@ public sealed class ContinuityValidationService(
|
||||
public Task RestoreAsync(int warningId) => warnings.RestoreAsync(warningId);
|
||||
}
|
||||
|
||||
public sealed class SceneMetricTypeService(IProjectRepository projects, ISceneMetricTypeRepository metricTypes) : ISceneMetricTypeService
|
||||
{
|
||||
public async Task<ProjectSceneMetricsViewModel?> GetAsync(int projectId)
|
||||
{
|
||||
var project = await projects.GetAsync(projectId);
|
||||
if (project is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ProjectSceneMetricsViewModel
|
||||
{
|
||||
Project = project,
|
||||
Metrics = await metricTypes.ListForManagementAsync(projectId),
|
||||
NewMetric = new SceneMetricTypeEditViewModel { ProjectID = projectId }
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<SceneMetricTypeEditViewModel?> GetEditAsync(int projectId, int metricTypeId)
|
||||
{
|
||||
var metric = await metricTypes.GetAsync(metricTypeId, projectId);
|
||||
return metric is null ? null : new SceneMetricTypeEditViewModel
|
||||
{
|
||||
MetricTypeID = metric.MetricTypeID,
|
||||
ProjectID = projectId,
|
||||
MetricName = metric.MetricName,
|
||||
Description = metric.Description,
|
||||
MinValue = metric.MinValue,
|
||||
MaxValue = metric.MaxValue,
|
||||
DefaultValue = metric.DefaultValue,
|
||||
SortOrder = metric.SortOrder,
|
||||
IsActive = metric.IsActive
|
||||
};
|
||||
}
|
||||
|
||||
public Task<int> SaveAsync(SceneMetricTypeEditViewModel model) => metricTypes.SaveAsync(new SceneMetricType
|
||||
{
|
||||
MetricTypeID = model.MetricTypeID,
|
||||
ProjectID = model.ProjectID,
|
||||
MetricName = model.MetricName,
|
||||
Description = model.Description,
|
||||
MinValue = model.MinValue,
|
||||
MaxValue = model.MaxValue,
|
||||
DefaultValue = model.DefaultValue,
|
||||
SortOrder = model.SortOrder,
|
||||
IsActive = model.IsActive
|
||||
});
|
||||
|
||||
public Task<bool> SetActiveAsync(int projectId, int metricTypeId, bool isActive) => metricTypes.SetActiveAsync(metricTypeId, projectId, isActive);
|
||||
|
||||
public Task<bool> MoveAsync(int projectId, int metricTypeId, string direction) => metricTypes.MoveAsync(metricTypeId, projectId, direction);
|
||||
|
||||
public Task AddMissingDefaultsAsync(int projectId) => metricTypes.AddMissingDefaultsAsync(projectId);
|
||||
|
||||
public Task<bool> RemoveUnusedAsync(int projectId, int metricTypeId) => metricTypes.RemoveUnusedAsync(metricTypeId, projectId);
|
||||
}
|
||||
|
||||
public sealed class AnalyticsService(IProjectRepository projects, IBookRepository books, IAnalyticsRepository analytics) : IAnalyticsService
|
||||
{
|
||||
public async Task<AnalyticsViewModel?> GetStoryHealthAsync(int projectId, int? bookId, decimal? startChapterNumber, decimal? endChapterNumber, decimal? startSceneNumber, decimal? endSceneNumber)
|
||||
|
||||
430
PlotLine/Sql/029_ProjectSceneMetricsManagement.sql
Normal file
430
PlotLine/Sql/029_ProjectSceneMetricsManagement.sql
Normal file
@ -0,0 +1,430 @@
|
||||
SET ANSI_NULLS ON;
|
||||
GO
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.SceneMetricType_ProjectHasMetrics
|
||||
@ProjectID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT CAST(CASE WHEN EXISTS
|
||||
(
|
||||
SELECT 1
|
||||
FROM dbo.ProjectTimelineMetricSettings ptms
|
||||
INNER JOIN dbo.SceneMetricTypes mt ON mt.MetricTypeID = ptms.MetricTypeID
|
||||
WHERE ptms.ProjectID = @ProjectID AND mt.IsActive = 1
|
||||
) THEN 1 ELSE 0 END AS bit);
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.SceneMetricType_AddMissingDefaultsForProject
|
||||
@ProjectID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.ProjectTimelineSettings WHERE ProjectID = @ProjectID)
|
||||
BEGIN
|
||||
INSERT dbo.ProjectTimelineSettings
|
||||
(ProjectID, ShowSceneCards, ShowMetricShape, ShowPlotLines, ShowStoryAssets, ShowCharacterAppearances, ShowWarnings)
|
||||
VALUES
|
||||
(@ProjectID, 1, 1, 1, 1, 1, 1);
|
||||
END;
|
||||
|
||||
INSERT dbo.ProjectTimelineMetricSettings (ProjectID, MetricTypeID, SortOrder)
|
||||
SELECT @ProjectID, mt.MetricTypeID, mt.SortOrder
|
||||
FROM dbo.SceneMetricTypes mt
|
||||
WHERE mt.ProjectID IS NULL
|
||||
AND mt.IsActive = 1
|
||||
AND mt.MetricName IN
|
||||
(
|
||||
N'Overall Intensity', N'Tension', N'Emotional Weight', N'Action', N'Darkness',
|
||||
N'Romance', N'Sexual Charge', N'Violence', N'Hope / Lightness', N'Comedy', N'Mystery'
|
||||
)
|
||||
AND NOT EXISTS
|
||||
(
|
||||
SELECT 1
|
||||
FROM dbo.ProjectTimelineMetricSettings ptms
|
||||
INNER JOIN dbo.SceneMetricTypes existing ON existing.MetricTypeID = ptms.MetricTypeID
|
||||
WHERE ptms.ProjectID = @ProjectID
|
||||
AND LOWER(existing.MetricName) = LOWER(mt.MetricName)
|
||||
);
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.SceneMetricType_ListForProjectManagement
|
||||
@ProjectID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
;WITH CandidateMetrics AS
|
||||
(
|
||||
SELECT mt.*
|
||||
FROM dbo.SceneMetricTypes mt
|
||||
WHERE mt.ProjectID = @ProjectID
|
||||
UNION
|
||||
SELECT mt.*
|
||||
FROM dbo.SceneMetricTypes mt
|
||||
WHERE mt.ProjectID IS NULL
|
||||
AND mt.MetricName IN
|
||||
(
|
||||
N'Overall Intensity', N'Tension', N'Emotional Weight', N'Action', N'Darkness',
|
||||
N'Romance', N'Sexual Charge', N'Violence', N'Hope / Lightness', N'Comedy', N'Mystery'
|
||||
)
|
||||
UNION
|
||||
SELECT mt.*
|
||||
FROM dbo.SceneMetricTypes mt
|
||||
INNER JOIN dbo.ProjectTimelineMetricSettings ptms ON ptms.MetricTypeID = mt.MetricTypeID
|
||||
WHERE ptms.ProjectID = @ProjectID
|
||||
)
|
||||
SELECT cm.MetricTypeID, cm.ProjectID, cm.MetricName, cm.Description, cm.MinValue, cm.MaxValue, cm.DefaultValue,
|
||||
COALESCE(ptms.SortOrder, cm.SortOrder) AS SortOrder,
|
||||
CAST(CASE WHEN cm.IsActive = 1 AND ptms.MetricTypeID IS NOT NULL THEN 1 ELSE 0 END AS bit) AS IsActive,
|
||||
CAST(CASE WHEN cm.IsActive = 1 AND ptms.MetricTypeID IS NOT NULL THEN 1 ELSE 0 END AS bit) AS IsEnabledForProject,
|
||||
COUNT(smv.SceneID) AS ValueCount
|
||||
FROM CandidateMetrics cm
|
||||
LEFT JOIN dbo.ProjectTimelineMetricSettings ptms ON ptms.ProjectID = @ProjectID AND ptms.MetricTypeID = cm.MetricTypeID
|
||||
LEFT JOIN dbo.SceneMetricValues smv ON smv.MetricTypeID = cm.MetricTypeID
|
||||
GROUP BY cm.MetricTypeID, cm.ProjectID, cm.MetricName, cm.Description, cm.MinValue, cm.MaxValue, cm.DefaultValue,
|
||||
cm.SortOrder, cm.IsActive, ptms.MetricTypeID, ptms.SortOrder
|
||||
ORDER BY
|
||||
CASE WHEN cm.IsActive = 1 AND ptms.MetricTypeID IS NOT NULL THEN 0 ELSE 1 END,
|
||||
COALESCE(ptms.SortOrder, cm.SortOrder),
|
||||
cm.MetricName;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.SceneMetricType_GetForProject
|
||||
@MetricTypeID int,
|
||||
@ProjectID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT mt.MetricTypeID, mt.ProjectID, mt.MetricName, mt.Description, mt.MinValue, mt.MaxValue, mt.DefaultValue,
|
||||
COALESCE(ptms.SortOrder, mt.SortOrder) AS SortOrder,
|
||||
CAST(CASE WHEN mt.IsActive = 1 AND ptms.MetricTypeID IS NOT NULL THEN 1 ELSE 0 END AS bit) AS IsActive,
|
||||
CAST(CASE WHEN mt.IsActive = 1 AND ptms.MetricTypeID IS NOT NULL THEN 1 ELSE 0 END AS bit) AS IsEnabledForProject,
|
||||
(SELECT COUNT(*) FROM dbo.SceneMetricValues smv WHERE smv.MetricTypeID = mt.MetricTypeID) AS ValueCount
|
||||
FROM dbo.SceneMetricTypes mt
|
||||
LEFT JOIN dbo.ProjectTimelineMetricSettings ptms ON ptms.ProjectID = @ProjectID AND ptms.MetricTypeID = mt.MetricTypeID
|
||||
WHERE mt.MetricTypeID = @MetricTypeID
|
||||
AND (mt.ProjectID IS NULL OR mt.ProjectID = @ProjectID);
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.SceneMetricType_SaveForProject
|
||||
@MetricTypeID int = NULL,
|
||||
@ProjectID int,
|
||||
@MetricName nvarchar(100),
|
||||
@Description nvarchar(500) = NULL,
|
||||
@MinValue int,
|
||||
@MaxValue int,
|
||||
@DefaultValue int,
|
||||
@SortOrder int,
|
||||
@IsActive bit = 1
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
IF @MinValue >= @MaxValue
|
||||
THROW 50010, 'Metric minimum must be less than maximum.', 1;
|
||||
|
||||
IF @DefaultValue < @MinValue OR @DefaultValue > @MaxValue
|
||||
THROW 50011, 'Metric default value must be inside the metric range.', 1;
|
||||
|
||||
IF @MetricTypeID IS NULL OR @MetricTypeID = 0
|
||||
BEGIN
|
||||
SELECT @MetricTypeID = MetricTypeID
|
||||
FROM dbo.SceneMetricTypes
|
||||
WHERE ProjectID = @ProjectID AND LOWER(MetricName) = LOWER(@MetricName);
|
||||
|
||||
IF @MetricTypeID IS NULL OR @MetricTypeID = 0
|
||||
BEGIN
|
||||
INSERT dbo.SceneMetricTypes (ProjectID, MetricName, Description, MinValue, MaxValue, DefaultValue, SortOrder, IsActive)
|
||||
VALUES (@ProjectID, @MetricName, @Description, @MinValue, @MaxValue, @DefaultValue, @SortOrder, 1);
|
||||
SET @MetricTypeID = CAST(SCOPE_IDENTITY() AS int);
|
||||
END
|
||||
END
|
||||
|
||||
UPDATE dbo.SceneMetricTypes
|
||||
SET MetricName = @MetricName,
|
||||
Description = @Description,
|
||||
MinValue = @MinValue,
|
||||
MaxValue = @MaxValue,
|
||||
DefaultValue = @DefaultValue,
|
||||
SortOrder = @SortOrder,
|
||||
IsActive = 1
|
||||
WHERE MetricTypeID = @MetricTypeID
|
||||
AND (ProjectID = @ProjectID OR ProjectID IS NULL);
|
||||
|
||||
IF @IsActive = 1
|
||||
BEGIN
|
||||
MERGE dbo.ProjectTimelineMetricSettings AS target
|
||||
USING (SELECT @ProjectID AS ProjectID, @MetricTypeID AS MetricTypeID, @SortOrder AS SortOrder) AS source
|
||||
ON target.ProjectID = source.ProjectID AND target.MetricTypeID = source.MetricTypeID
|
||||
WHEN MATCHED THEN UPDATE SET SortOrder = source.SortOrder
|
||||
WHEN NOT MATCHED THEN INSERT (ProjectID, MetricTypeID, SortOrder) VALUES (source.ProjectID, source.MetricTypeID, source.SortOrder);
|
||||
END
|
||||
ELSE
|
||||
BEGIN
|
||||
DELETE FROM dbo.ProjectTimelineMetricSettings WHERE ProjectID = @ProjectID AND MetricTypeID = @MetricTypeID;
|
||||
END;
|
||||
|
||||
SELECT @MetricTypeID;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.SceneMetricType_SetActiveForProject
|
||||
@MetricTypeID int,
|
||||
@ProjectID int,
|
||||
@IsActive bit
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
IF @IsActive = 1
|
||||
BEGIN
|
||||
UPDATE dbo.SceneMetricTypes
|
||||
SET IsActive = 1
|
||||
WHERE MetricTypeID = @MetricTypeID
|
||||
AND ProjectID = @ProjectID;
|
||||
|
||||
INSERT dbo.ProjectTimelineMetricSettings (ProjectID, MetricTypeID, SortOrder)
|
||||
SELECT @ProjectID, mt.MetricTypeID, mt.SortOrder
|
||||
FROM dbo.SceneMetricTypes mt
|
||||
WHERE mt.MetricTypeID = @MetricTypeID
|
||||
AND mt.IsActive = 1
|
||||
AND (mt.ProjectID IS NULL OR mt.ProjectID = @ProjectID)
|
||||
AND NOT EXISTS
|
||||
(
|
||||
SELECT 1 FROM dbo.ProjectTimelineMetricSettings ptms
|
||||
WHERE ptms.ProjectID = @ProjectID AND ptms.MetricTypeID = @MetricTypeID
|
||||
);
|
||||
|
||||
SELECT CAST(CASE WHEN EXISTS
|
||||
(
|
||||
SELECT 1
|
||||
FROM dbo.ProjectTimelineMetricSettings ptms
|
||||
INNER JOIN dbo.SceneMetricTypes mt ON mt.MetricTypeID = ptms.MetricTypeID
|
||||
WHERE ptms.ProjectID = @ProjectID
|
||||
AND ptms.MetricTypeID = @MetricTypeID
|
||||
AND mt.IsActive = 1
|
||||
AND (mt.ProjectID IS NULL OR mt.ProjectID = @ProjectID)
|
||||
) THEN 1 ELSE 0 END AS bit);
|
||||
RETURN;
|
||||
END
|
||||
ELSE
|
||||
BEGIN
|
||||
DELETE FROM dbo.ProjectTimelineMetricSettings
|
||||
WHERE ProjectID = @ProjectID AND MetricTypeID = @MetricTypeID;
|
||||
|
||||
UPDATE dbo.SceneMetricTypes
|
||||
SET IsActive = 0
|
||||
WHERE MetricTypeID = @MetricTypeID AND ProjectID = @ProjectID;
|
||||
|
||||
SELECT CAST(CASE WHEN NOT EXISTS
|
||||
(
|
||||
SELECT 1
|
||||
FROM dbo.ProjectTimelineMetricSettings
|
||||
WHERE ProjectID = @ProjectID AND MetricTypeID = @MetricTypeID
|
||||
) THEN 1 ELSE 0 END AS bit);
|
||||
RETURN;
|
||||
END;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.SceneMetricType_MoveForProject
|
||||
@MetricTypeID int,
|
||||
@ProjectID int,
|
||||
@Direction nvarchar(10)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
DECLARE @NormalisedDirection nvarchar(10) = CASE WHEN UPPER(@Direction) = N'UP' THEN N'Up' ELSE N'Down' END;
|
||||
|
||||
;WITH CandidateMetrics AS
|
||||
(
|
||||
SELECT mt.MetricTypeID, mt.ProjectID, mt.MetricName,
|
||||
COALESCE(ptms.SortOrder, mt.SortOrder) AS SortOrder,
|
||||
CAST(CASE WHEN mt.IsActive = 1 AND ptms.MetricTypeID IS NOT NULL THEN 1 ELSE 0 END AS bit) AS IsEnabledForProject,
|
||||
CAST(CASE WHEN mt.IsActive = 1 AND ptms.MetricTypeID IS NOT NULL THEN 0 ELSE 1 END AS int) AS GroupRank
|
||||
FROM dbo.SceneMetricTypes mt
|
||||
LEFT JOIN dbo.ProjectTimelineMetricSettings ptms ON ptms.ProjectID = @ProjectID AND ptms.MetricTypeID = mt.MetricTypeID
|
||||
WHERE
|
||||
mt.ProjectID = @ProjectID
|
||||
OR ptms.ProjectID = @ProjectID
|
||||
OR
|
||||
(
|
||||
mt.ProjectID IS NULL
|
||||
AND mt.MetricName IN
|
||||
(
|
||||
N'Overall Intensity', N'Tension', N'Emotional Weight', N'Action', N'Darkness',
|
||||
N'Romance', N'Sexual Charge', N'Violence', N'Hope / Lightness', N'Comedy', N'Mystery'
|
||||
)
|
||||
)
|
||||
)
|
||||
SELECT *,
|
||||
ROW_NUMBER() OVER (PARTITION BY GroupRank ORDER BY SortOrder, MetricName, MetricTypeID) AS RowNumber,
|
||||
ROW_NUMBER() OVER (PARTITION BY GroupRank ORDER BY SortOrder, MetricName, MetricTypeID) * 10 AS NormalisedSortOrder
|
||||
INTO #OrderedMetrics
|
||||
FROM CandidateMetrics;
|
||||
|
||||
UPDATE ptms
|
||||
SET SortOrder = ordered.NormalisedSortOrder
|
||||
FROM dbo.ProjectTimelineMetricSettings ptms
|
||||
INNER JOIN #OrderedMetrics ordered ON ordered.MetricTypeID = ptms.MetricTypeID
|
||||
WHERE ptms.ProjectID = @ProjectID
|
||||
AND ordered.IsEnabledForProject = 1;
|
||||
|
||||
UPDATE mt
|
||||
SET SortOrder = ordered.NormalisedSortOrder
|
||||
FROM dbo.SceneMetricTypes mt
|
||||
INNER JOIN #OrderedMetrics ordered ON ordered.MetricTypeID = mt.MetricTypeID
|
||||
WHERE mt.ProjectID = @ProjectID
|
||||
AND ordered.IsEnabledForProject = 0;
|
||||
|
||||
;WITH OrderedMetrics AS
|
||||
(
|
||||
SELECT MetricTypeID, ProjectID, MetricName, NormalisedSortOrder AS SortOrder, IsEnabledForProject, GroupRank, RowNumber
|
||||
FROM #OrderedMetrics
|
||||
),
|
||||
MovePair AS
|
||||
(
|
||||
SELECT currentMetric.MetricTypeID AS CurrentMetricTypeID,
|
||||
currentMetric.ProjectID AS CurrentProjectID,
|
||||
currentMetric.IsEnabledForProject AS CurrentIsEnabledForProject,
|
||||
currentMetric.SortOrder AS CurrentSortOrder,
|
||||
adjacentMetric.MetricTypeID AS AdjacentMetricTypeID,
|
||||
adjacentMetric.ProjectID AS AdjacentProjectID,
|
||||
adjacentMetric.IsEnabledForProject AS AdjacentIsEnabledForProject,
|
||||
adjacentMetric.SortOrder AS AdjacentSortOrder
|
||||
FROM OrderedMetrics currentMetric
|
||||
INNER JOIN OrderedMetrics adjacentMetric ON adjacentMetric.GroupRank = currentMetric.GroupRank
|
||||
AND adjacentMetric.RowNumber = CASE WHEN @NormalisedDirection = N'Up' THEN currentMetric.RowNumber - 1 ELSE currentMetric.RowNumber + 1 END
|
||||
WHERE currentMetric.MetricTypeID = @MetricTypeID
|
||||
)
|
||||
SELECT *
|
||||
INTO #MovePair
|
||||
FROM MovePair;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM #MovePair)
|
||||
BEGIN
|
||||
SELECT CAST(0 AS bit);
|
||||
RETURN;
|
||||
END;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM #MovePair WHERE CurrentIsEnabledForProject = 0 AND CurrentProjectID IS NULL)
|
||||
BEGIN
|
||||
SELECT CAST(0 AS bit);
|
||||
RETURN;
|
||||
END;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM #MovePair WHERE AdjacentIsEnabledForProject = 0 AND AdjacentProjectID IS NULL)
|
||||
BEGIN
|
||||
SELECT CAST(0 AS bit);
|
||||
RETURN;
|
||||
END;
|
||||
|
||||
UPDATE ptms
|
||||
SET SortOrder = mp.AdjacentSortOrder
|
||||
FROM dbo.ProjectTimelineMetricSettings ptms
|
||||
INNER JOIN #MovePair mp ON mp.CurrentMetricTypeID = ptms.MetricTypeID
|
||||
WHERE ptms.ProjectID = @ProjectID
|
||||
AND mp.CurrentIsEnabledForProject = 1;
|
||||
|
||||
UPDATE mt
|
||||
SET SortOrder = mp.AdjacentSortOrder
|
||||
FROM dbo.SceneMetricTypes mt
|
||||
INNER JOIN #MovePair mp ON mp.CurrentMetricTypeID = mt.MetricTypeID
|
||||
WHERE mt.ProjectID = @ProjectID
|
||||
AND mp.CurrentIsEnabledForProject = 0;
|
||||
|
||||
UPDATE ptms
|
||||
SET SortOrder = mp.CurrentSortOrder
|
||||
FROM dbo.ProjectTimelineMetricSettings ptms
|
||||
INNER JOIN #MovePair mp ON mp.AdjacentMetricTypeID = ptms.MetricTypeID
|
||||
WHERE ptms.ProjectID = @ProjectID
|
||||
AND mp.AdjacentIsEnabledForProject = 1;
|
||||
|
||||
UPDATE mt
|
||||
SET SortOrder = mp.CurrentSortOrder
|
||||
FROM dbo.SceneMetricTypes mt
|
||||
INNER JOIN #MovePair mp ON mp.AdjacentMetricTypeID = mt.MetricTypeID
|
||||
WHERE mt.ProjectID = @ProjectID
|
||||
AND mp.AdjacentIsEnabledForProject = 0;
|
||||
|
||||
SELECT CAST(1 AS bit);
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.SceneMetricType_RemoveUnusedForProject
|
||||
@MetricTypeID int,
|
||||
@ProjectID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
IF EXISTS
|
||||
(
|
||||
SELECT 1
|
||||
FROM dbo.SceneMetricTypes mt
|
||||
WHERE mt.MetricTypeID = @MetricTypeID
|
||||
AND mt.ProjectID = @ProjectID
|
||||
AND mt.IsActive = 0
|
||||
AND NOT EXISTS (SELECT 1 FROM dbo.SceneMetricValues smv WHERE smv.MetricTypeID = mt.MetricTypeID)
|
||||
)
|
||||
BEGIN
|
||||
DELETE FROM dbo.ProjectTimelineMetricSettings
|
||||
WHERE ProjectID = @ProjectID AND MetricTypeID = @MetricTypeID;
|
||||
|
||||
DELETE FROM dbo.SceneMetricTypes
|
||||
WHERE MetricTypeID = @MetricTypeID AND ProjectID = @ProjectID;
|
||||
|
||||
SELECT CAST(1 AS bit);
|
||||
RETURN;
|
||||
END;
|
||||
|
||||
SELECT CAST(0 AS bit);
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.ProjectTimelineSettings_EnsureDefaults
|
||||
@ProjectID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.Projects WHERE ProjectID = @ProjectID AND IsArchived = 0)
|
||||
RETURN;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.ProjectTimelineSettings WHERE ProjectID = @ProjectID)
|
||||
BEGIN
|
||||
INSERT dbo.ProjectTimelineSettings
|
||||
(ProjectID, ShowSceneCards, ShowMetricShape, ShowPlotLines, ShowStoryAssets, ShowCharacterAppearances, ShowWarnings)
|
||||
VALUES
|
||||
(@ProjectID, 1, 1, 1, 1, 1, 1);
|
||||
END;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.ProjectTimelineSettings_MetricTypes
|
||||
@ProjectID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT mt.MetricTypeID, mt.ProjectID, mt.MetricName, mt.Description, mt.MinValue, mt.MaxValue, mt.DefaultValue,
|
||||
ptms.SortOrder, mt.IsActive
|
||||
FROM dbo.ProjectTimelineMetricSettings ptms
|
||||
INNER JOIN dbo.SceneMetricTypes mt ON mt.MetricTypeID = ptms.MetricTypeID
|
||||
WHERE ptms.ProjectID = @ProjectID
|
||||
AND mt.IsActive = 1
|
||||
AND (mt.ProjectID IS NULL OR mt.ProjectID = @ProjectID)
|
||||
ORDER BY ptms.SortOrder, mt.SortOrder, mt.MetricName;
|
||||
END;
|
||||
GO
|
||||
@ -447,6 +447,41 @@ public sealed class TimelineMetricSettingOptionViewModel
|
||||
public bool IsSelected { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ProjectSceneMetricsViewModel
|
||||
{
|
||||
public Project Project { get; set; } = new();
|
||||
public IReadOnlyList<SceneMetricType> Metrics { get; set; } = [];
|
||||
public SceneMetricTypeEditViewModel NewMetric { get; set; } = new();
|
||||
public bool HasEnabledMetrics => Metrics.Any(x => x.IsEnabledForProject && x.IsActive);
|
||||
}
|
||||
|
||||
public sealed class SceneMetricTypeEditViewModel
|
||||
{
|
||||
public int MetricTypeID { get; set; }
|
||||
public int ProjectID { get; set; }
|
||||
|
||||
[Required, StringLength(100)]
|
||||
[Display(Name = "Metric name")]
|
||||
public string MetricName { get; set; } = string.Empty;
|
||||
|
||||
public string? Description { get; set; }
|
||||
|
||||
[Display(Name = "Minimum value")]
|
||||
public int MinValue { get; set; } = 1;
|
||||
|
||||
[Display(Name = "Maximum value")]
|
||||
public int MaxValue { get; set; } = 10;
|
||||
|
||||
[Display(Name = "Default value")]
|
||||
public int DefaultValue { get; set; } = 5;
|
||||
|
||||
[Display(Name = "Sort order")]
|
||||
public int SortOrder { get; set; } = 100;
|
||||
|
||||
[Display(Name = "Active")]
|
||||
public bool IsActive { get; set; } = true;
|
||||
}
|
||||
|
||||
public sealed class StoryBibleViewModel
|
||||
{
|
||||
public Project Project { get; set; } = new();
|
||||
|
||||
61
PlotLine/Views/ProjectMetrics/Edit.cshtml
Normal file
61
PlotLine/Views/ProjectMetrics/Edit.cshtml
Normal file
@ -0,0 +1,61 @@
|
||||
@model SceneMetricTypeEditViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Edit Scene Metric";
|
||||
}
|
||||
|
||||
<div class="page-heading compact">
|
||||
<div>
|
||||
<p class="eyebrow">Project Settings</p>
|
||||
<h1>Edit scene metric</h1>
|
||||
</div>
|
||||
<a class="btn btn-outline-secondary" asp-action="Index" asp-route-projectId="@Model.ProjectID">Back to metrics</a>
|
||||
</div>
|
||||
|
||||
<form asp-action="Save" method="post" class="edit-panel">
|
||||
<input asp-for="MetricTypeID" type="hidden" />
|
||||
<input asp-for="ProjectID" type="hidden" />
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="MetricName" class="form-label"></label>
|
||||
<input asp-for="MetricName" class="form-control" />
|
||||
<span asp-validation-for="MetricName" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label asp-for="Description" class="form-label"></label>
|
||||
<textarea asp-for="Description" class="form-control" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label asp-for="MinValue" class="form-label"></label>
|
||||
<input asp-for="MinValue" class="form-control" />
|
||||
<span asp-validation-for="MinValue" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label asp-for="MaxValue" class="form-label"></label>
|
||||
<input asp-for="MaxValue" class="form-control" />
|
||||
<span asp-validation-for="MaxValue" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label asp-for="DefaultValue" class="form-label"></label>
|
||||
<input asp-for="DefaultValue" class="form-control" />
|
||||
<span asp-validation-for="DefaultValue" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label asp-for="SortOrder" class="form-label"></label>
|
||||
<input asp-for="SortOrder" class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check mt-3">
|
||||
<input asp-for="IsActive" class="form-check-input" />
|
||||
<label asp-for="IsActive" class="form-check-label"></label>
|
||||
</div>
|
||||
<div class="button-row mt-3">
|
||||
<button class="btn btn-primary" type="submit">Save Metric</button>
|
||||
<a class="btn btn-outline-secondary" asp-action="Index" asp-route-projectId="@Model.ProjectID">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
182
PlotLine/Views/ProjectMetrics/Index.cshtml
Normal file
182
PlotLine/Views/ProjectMetrics/Index.cshtml
Normal file
@ -0,0 +1,182 @@
|
||||
@model ProjectSceneMetricsViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Scene Metrics";
|
||||
}
|
||||
|
||||
<nav class="breadcrumb-trail">
|
||||
<a asp-controller="Projects" asp-action="Details" asp-route-id="@Model.Project.ProjectID">@Model.Project.ProjectName</a>
|
||||
<span>Scene Metrics</span>
|
||||
</nav>
|
||||
|
||||
<div class="page-heading compact">
|
||||
<div>
|
||||
<p class="eyebrow">Project Settings</p>
|
||||
<h1>Scene Metrics</h1>
|
||||
<p class="lead-text">Create and manage the metric sliders available when editing scenes.</p>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<a class="btn btn-outline-secondary" asp-controller="Timeline" asp-action="Settings" asp-route-projectId="@Model.Project.ProjectID">Timeline Settings</a>
|
||||
<a class="btn btn-outline-secondary" asp-controller="Projects" asp-action="Details" asp-route-id="@Model.Project.ProjectID">Back to project</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (TempData["ProjectMetricMessage"] is string message)
|
||||
{
|
||||
<div class="alert alert-info">@message</div>
|
||||
}
|
||||
|
||||
<section class="story-bible-section">
|
||||
<div class="story-bible-section-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Defaults</p>
|
||||
<h2>Add missing default metrics</h2>
|
||||
</div>
|
||||
</div>
|
||||
<p class="muted">Adds any missing core metrics for this project without duplicating existing metrics.</p>
|
||||
<form asp-action="AddMissingDefaults" method="post">
|
||||
<input type="hidden" name="projectId" value="@Model.Project.ProjectID" />
|
||||
<button class="btn btn-outline-primary" type="submit">Add missing default metrics</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="story-bible-section">
|
||||
<div class="story-bible-section-heading">
|
||||
<div>
|
||||
<p class="eyebrow">New Metric</p>
|
||||
<h2>Add scene metric</h2>
|
||||
</div>
|
||||
</div>
|
||||
<form asp-action="Save" method="post" class="row g-3">
|
||||
<input asp-for="NewMetric.ProjectID" type="hidden" name="ProjectID" />
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="MetricName">Metric name</label>
|
||||
<input class="form-control" id="MetricName" name="MetricName" maxlength="100" required />
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<label class="form-label" for="Description">Description</label>
|
||||
<input class="form-control" id="Description" name="Description" maxlength="500" />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label" for="MinValue">Min</label>
|
||||
<input class="form-control" id="MinValue" name="MinValue" type="number" value="1" />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label" for="MaxValue">Max</label>
|
||||
<input class="form-control" id="MaxValue" name="MaxValue" type="number" value="10" />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label" for="DefaultValue">Default</label>
|
||||
<input class="form-control" id="DefaultValue" name="DefaultValue" type="number" value="5" />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label" for="SortOrder">Sort order</label>
|
||||
<input class="form-control" id="SortOrder" name="SortOrder" type="number" value="100" />
|
||||
</div>
|
||||
<div class="col-md-2 d-flex align-items-end">
|
||||
<label class="form-check mb-2">
|
||||
<input class="form-check-input" name="IsActive" type="checkbox" value="true" checked />
|
||||
<span class="form-check-label">Active</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-md-2 d-flex align-items-end">
|
||||
<button class="btn btn-primary w-100" type="submit">Add Metric</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="story-bible-section">
|
||||
<div class="story-bible-section-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Available Metrics</p>
|
||||
<h2>Project scene metrics</h2>
|
||||
</div>
|
||||
<span class="soft-count">@Model.Metrics.Count metric@(Model.Metrics.Count == 1 ? "" : "s")</span>
|
||||
</div>
|
||||
|
||||
@if (!Model.Metrics.Any())
|
||||
{
|
||||
<div class="empty-panel compact">
|
||||
<p>This project has no scene metrics yet.</p>
|
||||
<form asp-action="AddMissingDefaults" method="post">
|
||||
<input type="hidden" name="projectId" value="@Model.Project.ProjectID" />
|
||||
<button class="btn btn-primary" type="submit">Add default metrics</button>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Metric</th>
|
||||
<th>Range</th>
|
||||
<th>Default</th>
|
||||
<th>Sort</th>
|
||||
<th>Status</th>
|
||||
<th>Values</th>
|
||||
<th class="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var metric in Model.Metrics)
|
||||
{
|
||||
var canMove = metric.IsEnabledForProject || metric.ProjectID == Model.Project.ProjectID;
|
||||
var canRemove = !metric.IsActive && metric.ProjectID == Model.Project.ProjectID && metric.ValueCount == 0;
|
||||
<tr class="@(metric.IsActive ? "" : "text-muted")">
|
||||
<td>
|
||||
<strong>@metric.MetricName</strong>
|
||||
@if (!string.IsNullOrWhiteSpace(metric.Description))
|
||||
{
|
||||
<div class="muted">@metric.Description</div>
|
||||
}
|
||||
</td>
|
||||
<td>@metric.MinValue-@metric.MaxValue</td>
|
||||
<td>@metric.DefaultValue</td>
|
||||
<td>@metric.SortOrder</td>
|
||||
<td>
|
||||
<span class="status-pill">@(metric.IsActive ? "Active" : "Inactive")</span>
|
||||
</td>
|
||||
<td>@metric.ValueCount</td>
|
||||
<td>
|
||||
<div class="button-row justify-content-end">
|
||||
<form asp-action="Move" method="post">
|
||||
<input type="hidden" name="projectId" value="@Model.Project.ProjectID" />
|
||||
<input type="hidden" name="id" value="@metric.MetricTypeID" />
|
||||
<input type="hidden" name="direction" value="Up" />
|
||||
<button class="btn btn-outline-secondary btn-sm" type="submit" disabled="@(!canMove)">Up</button>
|
||||
</form>
|
||||
<form asp-action="Move" method="post">
|
||||
<input type="hidden" name="projectId" value="@Model.Project.ProjectID" />
|
||||
<input type="hidden" name="id" value="@metric.MetricTypeID" />
|
||||
<input type="hidden" name="direction" value="Down" />
|
||||
<button class="btn btn-outline-secondary btn-sm" type="submit" disabled="@(!canMove)">Down</button>
|
||||
</form>
|
||||
<a class="btn btn-outline-secondary btn-sm" asp-action="Edit" asp-route-projectId="@Model.Project.ProjectID" asp-route-id="@metric.MetricTypeID">Edit</a>
|
||||
<form asp-action="SetActive" method="post">
|
||||
<input type="hidden" name="projectId" value="@Model.Project.ProjectID" />
|
||||
<input type="hidden" name="id" value="@metric.MetricTypeID" />
|
||||
<input type="hidden" name="isActive" value="@(metric.IsActive ? "false" : "true")" />
|
||||
<button class="btn btn-outline-secondary btn-sm" type="submit">@(metric.IsActive ? "Deactivate" : "Reactivate")</button>
|
||||
</form>
|
||||
@if (canRemove)
|
||||
{
|
||||
<form asp-action="Remove" method="post"
|
||||
data-confirm-title="Remove unused metric?"
|
||||
data-confirm-body="This metric has no scene values and can be removed safely."
|
||||
data-confirm-button-text="Remove"
|
||||
data-confirm-button-class="btn-danger">
|
||||
<input type="hidden" name="projectId" value="@Model.Project.ProjectID" />
|
||||
<input type="hidden" name="id" value="@metric.MetricTypeID" />
|
||||
<button class="btn btn-outline-danger btn-sm" type="submit">Remove</button>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
@ -16,6 +16,7 @@
|
||||
<nav class="context-nav" aria-label="Project navigation">
|
||||
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Timeline</a>
|
||||
<a asp-controller="Timeline" asp-action="Settings" asp-route-projectId="@Model.Project.ProjectID">Timeline Settings</a>
|
||||
<a asp-controller="ProjectMetrics" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Scene Metrics</a>
|
||||
<a asp-controller="Scenarios" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Scenarios</a>
|
||||
<a asp-controller="Writer" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Writer Workspace</a>
|
||||
<a asp-controller="StoryBible" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Story Bible</a>
|
||||
@ -44,6 +45,7 @@
|
||||
<a class="btn btn-outline-secondary" asp-action="Edit" asp-route-id="@Model.Project.ProjectID">Edit project</a>
|
||||
<a class="btn btn-outline-primary" asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Timeline</a>
|
||||
<a class="btn btn-outline-secondary" asp-controller="Timeline" asp-action="Settings" asp-route-projectId="@Model.Project.ProjectID">Timeline Settings</a>
|
||||
<a class="btn btn-outline-secondary" asp-controller="ProjectMetrics" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Scene Metrics</a>
|
||||
<a class="btn btn-outline-primary" asp-controller="Scenarios" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Scenarios</a>
|
||||
<a class="btn btn-outline-primary" asp-controller="Writer" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Writer Workspace</a>
|
||||
<a class="btn btn-outline-primary" asp-controller="StoryBible" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID">Story Bible</a>
|
||||
|
||||
@ -195,24 +195,37 @@
|
||||
|
||||
<div class="inspector-section" data-section-title="Scene Metrics" data-section-id="inspector-metrics">
|
||||
<h3>Scene Metrics</h3>
|
||||
<div class="metric-slider-stack">
|
||||
@for (var index = 0; index < Model.Metrics.Count; index++)
|
||||
{
|
||||
<div class="metric-slider">
|
||||
<input asp-for="Metrics[index].MetricTypeID" type="hidden" />
|
||||
<input asp-for="Metrics[index].MetricName" type="hidden" />
|
||||
<input asp-for="Metrics[index].Description" type="hidden" />
|
||||
<input asp-for="Metrics[index].MinValue" type="hidden" />
|
||||
<input asp-for="Metrics[index].MaxValue" type="hidden" />
|
||||
<div class="metric-slider-label">
|
||||
<label asp-for="Metrics[index].Value">@Model.Metrics[index].MetricName</label>
|
||||
<output>@Model.Metrics[index].Value</output>
|
||||
@if (!Model.Metrics.Any())
|
||||
{
|
||||
<div class="empty-panel compact">
|
||||
<p>This project has no scene metrics yet.</p>
|
||||
@if (Model.ReturnProjectID.HasValue)
|
||||
{
|
||||
<a class="btn btn-outline-primary btn-sm" asp-controller="ProjectMetrics" asp-action="Index" asp-route-projectId="@Model.ReturnProjectID">Add default metrics</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="metric-slider-stack">
|
||||
@for (var index = 0; index < Model.Metrics.Count; index++)
|
||||
{
|
||||
<div class="metric-slider">
|
||||
<input asp-for="Metrics[index].MetricTypeID" type="hidden" />
|
||||
<input asp-for="Metrics[index].MetricName" type="hidden" />
|
||||
<input asp-for="Metrics[index].Description" type="hidden" />
|
||||
<input asp-for="Metrics[index].MinValue" type="hidden" />
|
||||
<input asp-for="Metrics[index].MaxValue" type="hidden" />
|
||||
<div class="metric-slider-label">
|
||||
<label asp-for="Metrics[index].Value">@Model.Metrics[index].MetricName</label>
|
||||
<output>@Model.Metrics[index].Value</output>
|
||||
</div>
|
||||
<input asp-for="Metrics[index].Value" type="range" min="@Model.Metrics[index].MinValue" max="@Model.Metrics[index].MaxValue" class="form-range" oninput="this.previousElementSibling.querySelector('output').value = this.value" />
|
||||
<input asp-for="Metrics[index].Notes" class="form-control form-control-sm" placeholder="Metric note" />
|
||||
</div>
|
||||
<input asp-for="Metrics[index].Value" type="range" min="@Model.Metrics[index].MinValue" max="@Model.Metrics[index].MaxValue" class="form-range" oninput="this.previousElementSibling.querySelector('output').value = this.value" />
|
||||
<input asp-for="Metrics[index].Notes" class="form-control form-control-sm" placeholder="Metric note" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="inspector-actions">
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
</div>
|
||||
<nav class="context-nav" aria-label="Timeline settings navigation">
|
||||
<a asp-controller="Timeline" asp-action="Index" asp-route-projectId="@Model.ProjectID">Open timeline</a>
|
||||
<a asp-controller="ProjectMetrics" asp-action="Index" asp-route-projectId="@Model.ProjectID">Scene Metrics</a>
|
||||
<a asp-controller="Projects" asp-action="Details" asp-route-id="@Model.ProjectID">Project</a>
|
||||
</nav>
|
||||
</section>
|
||||
@ -102,21 +103,31 @@
|
||||
</div>
|
||||
<p class="muted">These metrics appear on the timeline graph and Story Health metric shape for this project.</p>
|
||||
<span asp-validation-for="SelectedMetricTypeIDs" class="text-danger"></span>
|
||||
<div class="timeline-settings-grid timeline-metric-settings-grid">
|
||||
@foreach (var metric in Model.MetricOptions)
|
||||
{
|
||||
<label class="timeline-setting-card metric-setting-card">
|
||||
<input type="checkbox" name="SelectedMetricTypeIDs" value="@metric.MetricTypeID" checked="@metric.IsSelected" />
|
||||
<span>
|
||||
<strong>@metric.MetricName</strong>
|
||||
@if (!string.IsNullOrWhiteSpace(metric.Description))
|
||||
{
|
||||
<small>@metric.Description</small>
|
||||
}
|
||||
</span>
|
||||
</label>
|
||||
}
|
||||
</div>
|
||||
@if (!Model.MetricOptions.Any())
|
||||
{
|
||||
<div class="empty-panel compact">
|
||||
<p>This project has no scene metrics yet.</p>
|
||||
<a class="btn btn-primary" asp-controller="ProjectMetrics" asp-action="Index" asp-route-projectId="@Model.ProjectID">Add default metrics</a>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="timeline-settings-grid timeline-metric-settings-grid">
|
||||
@foreach (var metric in Model.MetricOptions)
|
||||
{
|
||||
<label class="timeline-setting-card metric-setting-card">
|
||||
<input type="checkbox" name="SelectedMetricTypeIDs" value="@metric.MetricTypeID" checked="@metric.IsSelected" />
|
||||
<span>
|
||||
<strong>@metric.MetricName</strong>
|
||||
@if (!string.IsNullOrWhiteSpace(metric.Description))
|
||||
{
|
||||
<small>@metric.Description</small>
|
||||
}
|
||||
</span>
|
||||
</label>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
<div class="button-row">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user