Project restore working
This commit is contained in:
parent
aebaa00371
commit
f8bf5af17f
@ -1,5 +0,0 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACoxUzW7bOBB+FYFnUaEoihIF7MFwWtRI0xiJu7vojSKHMRFZNCjKaRD43ReU5dopetibMCJnvr/hO+rlDlCDFvv9pwP0YUApCnYqUUI5JhwTtslFw3hDaJYLzhkVP1CK7B28oQZVTKlcCcAFcMBMkxK3hVFYaxA5yTWhJYs95fOAmnckbabhYBVkbvgb/GBdjxr0j+21e42zpc1U50adedfBqh+C7FUE8221vMNfF+vNw/p0aoAhXs6sRg0qZNFWihhcKOCYFUbiGpTBVEhWAi9pySMIaTPbB/C97LJBv1zmaxd6CKqhGS0ygqlAxxRpGWSE3MoBNm/7iGKS6DbW06k8fTbv6AAeNTQ9i3lqd6M6e7Mb2tF2+iZI/wzBeLmDV+df4CA7lKK9d3vwwcKkzcNTcoGUk4xklFNCUIrupdraHpLVbfLQRcJS1YSBLrAAozETucSCFQXWRSkryk1VUYNSdAe+h+6q671V3g3OhGSWPPkwZ+n6YPvRjUOy6gM8exlO9z7LbgCUovXYdnbYPoLUbxv3OMZ//dhFLsvRe+hDspZhm3yRwxY1SMha5VJIISVXykBJwBBNJCeVBq45aEZ1pZlgmjNBFGeyzZmWZd6aWEEpehjDfgxn/X9egdh4u9uBviDYTBqvOxmM87uVhj5YY6M15xNToLpuYpXMPZedhT58EBk1iORAdVEDNjTnmNVaYlkWAnNecAO5EhXNT4IZ+zz+0okYo1lhyrrmrWhzAtRUNSOVaYlWplBCly1UtCJVDbVQhLQly+tWKKIhL5mO7LzTowq/R6GYDHoc+7idySrSfrU9/jkFe1Zk7V1wynXRMNvB09sQYPcn15Zut3eDDZG/ma39PsC96+Pfef9nzebLCxcuNYh7kEyrJ2jBCsY1JqximDFTY1GWGjNatsKYNudFgVJ069QL+CQGTNp+8uScqYenZOHV1gZQYfRx8r8TqZOdn88rc9Ej+/Zps3QeFvt9Olf/OkSRTs3OAfjwrnwf4vGtG8IV5ZnaE3TmDOwqThoOp6fKxiLXugbNc0xZXmImRI5rUVa4repWtLyGUl3imjyCth5UmPpt/Binzcr+KZcLH6yRKgxxeb46NcVpzud85HeQV7b9un0afmXdHKW5U05iZJ9OD+eUoP/1bh6Px+N/AAAA//8DACFE8GcoBgAA
|
||||
@ -1,5 +0,0 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpSTTW/cOAyG/4rBs+Xo25ZvwaSLHWTbDNpBd7E3WaIbIY48kOW0RTD/vZBnijZ724sgECL5vg+pV4j2GaGH29Pp3QvGvEANOWwhTrkmVBMqj8z0UveMNqoTujP6X6gh3ON36KGVzjFnkAjUSKSnigxidMR7NIwyT7mSpab9skD/CjY0Hl+Cw2ZePmNawhyhh79D9PPX0tuGxk3z6ps0T7iPS7bRFTEf9rt78tft4fhwuLxacCnJTfDQg7BiaB0diXCoiRSjJR26kXBjpUKtuNJFhA1NiBlTtFOz+Kdf/f2cI2bX84aLhhJu4FyDt9kWyYNd8Pj9VFRsiO5KvN7C27V/hRdM0PP6J8xLuRs3hZvnZVjD5G8uZ7bLU3F5SvMJUw64MdlsTpPNYY7VtdNuChgz1PDwqTpMNo9zen4D6tPF//5/2D+W7u++oVsz+t28xgw9MEmhhvfWPYaI1f4OeqAMuRcdkpEzTWTnLbFKGKK10CMyZ1rOoIbdHHOI67wu1T5m/JI2A9DDH3Za8CL9NrnHkNHlNRVX/2w6Dmn2q8vVL/6MNrQRtCh5WPNpzdVH9CGhy1j8HdN6rfefFK75lrRbU8KYq4PNj9WfdnmEHoztHLPGGmu1cyMqiiP11GraetReo5fct14a6bU01GlpBya9VWwYSwRqwDLuatswL5VrR8mJ4iMSKbwgHSpBqOCqVa1sFdVvOFYPU8mzrqMSvSAGR0+kYZYYKQTxQtmW67Ft+fgbkuv0GS2APb5c/sqmQHvfodeMcMkUkcYw0hnVkqHtBjPoDpUrmu8xRZx+A/U+uDQv85ir6/JUb9B9XGP579W2SF9DJN+2Gd3N7glTVWZsQyzrfR3r+Xw+/wAAAP//AwD+oJloNgQAAA==
|
||||
@ -1,5 +0,0 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpSUTW/cNhCG/4rAsyjzS5Som7FO0IWbepEs0iC3ETmKCcvUgqScGIb/e0HtFo5PRS8EMeLMvHzmpV5IgEckA7k+nT48YciJ1CT7LSSY0JRpytSRm0HpgbOm7TnTSnwnNfG3+EwG0ilruTVIJWqkyrGWjnKy1Dk0nHHHRKtKTfiRyPBCwDcOn7zFZklfMSa/BDKQv31wy8/SG3xj52V1TVxm3IeUIdgi5q/97pb+eX043h3OpxKmktx4RwYiQY6dZROVFjVVcgLao52oMKBa1K1odREBvvEhYwwwN8k9vPV3Sw6Y7SAaIRtGhSGvNXGQoUgeIeHx+VRUbIhuSrzewtt2eCFPGMkg6n9hnstd2dlfPaZx9bO7Oq8Z0kPa1glsXuIzqckpLieM2ePG5zolfBzn5yOkh4/nM2WbPvxCu2Z0u2UNmQyEy47UZAM0z5D9EqqLxt3sMeTtY44+JG//q5YkNbn7Uh1myNMSH9/N48sZ8/5/UP4E9t4HrPY3ZCCMo3CyRzoJrqnqHVBopaFaSz0ht6YTnNRkt4Tsw7qsqdqHjD/idiUykI8wJzzru4723me0eY3lnt+2Zoe4uNXm6m2WnDWskYyVpDWf1lx9Rucj2ozlEse4Xup9fZ8itNiSdmuMGHJ1gHxf/QHpngzEQG85GDAA2toJW4YTcww06xxqp9Ep4TqnjHJaGWa1gpErBy0fpxIhNcFinWpz69RxJjTr6WR5R5XTnILkhgqpuetlxwwUg71xrO7mkge2ZwqdpAYnR5XhQI2SkjrZQif01HVi+g3JxQ+cFcAOn87vblOgneux9BWKt1QZw2lv2o6OXT+aUffY2qL5FmPA+TdQn7yNS1qmXF0cUr1D93kN5d9RbW756QP9tc3oZrEPGKsyY/ChPJXLWF9fX1//AQAA//8DAO7pQJCCBAAA
|
||||
@ -1,5 +0,0 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACoxUXW/lJhD9KxavNV7AGIPf0iSrXm2zifZetVXfxjAkNA6+wji7qyj/vcI32U9V6otlDTDnzJkz80QiPCAZyNnxePmIMS+kJjlsIcGEokxRJg/cDFINnDWdVsIw8TepSXiHn8lAemkttwZpiwqpdKyjY+stdQ4NZ9wx0cmSE24XMjwRCI3Dx2CxmZc/MC1hjmQgf4bo5o8FG0Jjp3l1TZon3MUlQ7SFzPvd+Tv6+9nN4frmdGvBpTxugiMDaaEde8s8bS0qKlsPVKP1VBiQHapOdKqQgNCEmDFFmJrF3X/Fd3OOmO0gGtE2jApDnmviIEOhPMKCh8/HwmKT6KLE6y28/Q5P5BETGUT9KuYp3Rs7hTcPy7iGyb3ZvqQmxzQfMeWAmxrX++orCc4a1gglGCM1uQJ7FyJWu4vqeiolgtVMomupQe+oNByokW1LXdtBL5Tve+FJTd5hijh9k/Uq2DQvs8/Vi8jVdzj7s/PLCOOEBeMtTAuSmuyWfYboYJojXn5Cu+ZTrkNay/H5HHOI67wu1S5mvE3wcv76/nxNCWOubiDfVb/BckcGYkBbDgYMgLLWY8fQM8dAsd6hcgqdFK530kinpGFWSRi5dNDx0ZcIqcmvRcP9ai0uy1c2m0mmaeNQvfTpfAoY83cykoEwjsK1GqkXXFGpHVDoWkOVapVHbk0v+CvMAdItZjIQr8dxZNaOQhpuQRhonWetU94I0fcgfdv1UijVsR6s63vBpWt7tND6lmlBanKTZrfa/GOr260BN2n+B20uUpGBKNZ12sI4CiW5BqNRciW1Mtgb6YHJ1o7AOw3jyBl0zBotLHBlQI6u819Uuoy3IeJFWI4TfP4GVzeqaX+xAJp7kD9cf5vgAT/O6f79ycbN+8sDqcmHNZaNUO2KRT6GSD9tw3S1Tjkc7hKCQ3c1O/zZR1jGpdomVCjF9QgjZWpkVPrOU4Oqox57zk2vi6FJTS5me4+pKg6DEMtUfUl2va/Okr0LGW1eU+H318bjel/dTJD9nB6+2yTfFPajAI1oOSudcfh42kYbR+WcRqc4FZJ3VBrDqTZdT8dej2ZUGjtbFLte83HN1Qd0IaHNW8UvXtwwz+/Q3v8sxqsLXizKWfHa/rTFNmn/1xJ7rskDwrImfNi29fBEdjFi2pAv1tMo7uJVmKawoJ2jW8igdcubUs8Lw/++Z1SjtODPz8/P/wIAAP//AwA2Sn9NGwYAAA==
|
||||
@ -1,5 +0,0 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpRTy27cOBD8FYFnUeZLlKibMd5FBk7Wg8RIgtxaZCtDRCYHFOUkMObfF9RM4GRveyOK7O7qquILCfCEZCC3p9NfzxjyQmqS/QYJJjRlmjL1yM2g9MBZ0/a9Ml37hdTE3+NPMpBOWcutQSpRI1WOtXSUk6XOoeGMOyZaVXrC14UMLwR84/DZW2zi8hHT4mMgA/nkg4vfy2zwjZ3j6poUZ9yHJUOwhcw/+909fXt7eHw4XF4tuJTixjsyEAly7CybqLSoqZIT0B7tRIUB1aJuRasLCfCNDxlTgLlZ3LfX+S7mgNkOohGyYVQYcq6JgwyF8ggLPv48FRabRHcFrzd4Ow4v5BkTGUT9S8xLuxs7+xsbn54guJvJB78cSU1OKZ4wZY+bHNuG8wzZx1Bdh+xmjyGTmjx8qA4z5Cmmpz80+nBZff8/Nn8H9ugDVvs7MhDGUTjZI50E11T1Dii00lCtpZ6QW9MJTmqyiyH7sMZ1qfYh49e0sSQD+RvmBS/8bpM9+ow2r6lQ/7wNwx8+76IrACM1OaToVpurV7k5a1gjWbl8WPNpzdV7dD6hzVh2ekzrtf1/SoQWW9FuTQlDrg6Qj9UbWI5kIAZ6y8GAAdDWTtgynJhjoFnnUDuNTgnXOWWU08owqxWMXDlo+TgVpPAu7lZboJzknXYMqBXAqBIcqXFC0rGfhATTj1KWDLzKWj3MpQ5szxQ6SQ1OjirDgRolJXWyhU7oqevE9JskV8c5K3o7fL58jY2Bdq5HpzkVirdUGcNpb9qOjl0/mlH32NrC+R5TwPk3od55m+ISp1xdA1P9Id37NZTvXW3h+e4D/bFZdhftN0xVsRx8KGm+unw+n8//AgAA//8DAMd6qbMlBAAA
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACoxUS2/bOBD+KwLPokJSFPUA9mA4LdZI0xiJu7vYG0UOYyKyaFCU06DIf19Qkmun6GFvwmhe32P4A/XyAKhBq+Px0wn6MKAUBTuFGGECE4FJsaNFk4uGFxnjlFQV+xelyN7BG2pQyZWiqgacgwDMNSlwmxuFtYaaEqoJK3jsKZ8H1PxA0mYaTlZB5oa/wA/W9ahBf9teu9c4W9pMdW7UmXcdbPohyF7FZb5u1nf4y2q7e9jOWQMMsTizGjWIaEoo1QxrSjjmOaW4FVBgnatCqoqJnJRzme0D+F522aBfLvO1Cz0E1bCM5RnBrEbvKdIyyLhyKwfYvR3jFhNFtzGeTuHps/mBTuBRw9IzmXO7G9XZm8PQjrbTN0H6ZwjGywO8Ov8CJ9mhFB29O4IPFiZuHp6Sy0qUZCRjghGCUnQv1d72kGxuk4cuApaqIhx0jmswGvOaSlzzPMc6L2TJhClLZlCK7sD30F11vbfKu8GZkCyUJx/mrF0fbD+6cUg2fYBnL8Nc91l2A6AUbce2s8P+EaR+27nHMf7rxy5iWY/eQx+SrQz75E857FGDalkpKmtZSymUMlAQMEQTKUipQWgBmjNdal5zLXhNlOCypVzLgrYmRlCKHsZwHMOZ/+9XS+y8PRxAXzbYTRxvOxmM84eNhj5YY6M054zJUF03oUqWnuvOQh8+kBwdRYHpvAJsGBWYV1piWeQ1FiIXBqiqS0Znwox9Hn/yRIzRPDdFVYm2bikBZsqKk9K0RCuTq1oXLZSsJGUFVa0IaQtOq7ZWRAMtuI7ovNOjCr9aIZ8Eehz7eJ3JJsJ+tT3+LviFka13wSnXRcFsB09vQ4DD71Rbu8PRDTZE/GaR9tsA966Pf5f7XzhbilcuXGIQ7yCZTk+DKouKaMxNLjBvKcdSCIrrqmV5wQmRJKK6deoFfBINJm0/aXL21MNTsvJqbwOoMPo4+Z8J1Czn5/PJXPjIvn7arZ2H1fGYLtE/TpGkudnZAB/elW9DTN+7IaIIfrzy0RN05rzXlZs0nOaXysag0LoCLShmnBaY1zXFVV2UuC2rtm5FBYW6uDV5BG09qDD1283TFmJ/Z8uVD9ZIFYZ4O1+cmty02HNJ+XXJK9V+Vs/Dr5RbnLR0oiQ69ml+NycD/a9n8/39/f0/AAAA//8DADRAO7QnBgAA
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpRTTW/bOBD9KwLPpsIvUaJugbOLGmk3Rhu0i72NOaOaqEIZFJW2CPzfF5RdtOmtN+KRb+bNe8MXFuGJWM9uT6e/ninmmW1YDiukhLJcWC6aR9n02vaNq7Vx1mjzH9uwcE/fWc9a4730jrgmS9ygaPhBD54jkpNColCNKTXh88z6FwahRnoOnupp/khpDlNkPfsUIk5fS28ItR+nBes0jbSLc4boi5h/dtt7/vZ2//iwv7yaaS7kOiDrmUAppETFUQrDjZaSHyw1HLVvwHfKatFeaCFmShHGesYvP/vjlCNl36ta6Vpw5dh5wxAyFMkHmOnx+6moWC26K/hmhddj/8KeKbFebX6YeSl348dw46enJ4h4M4QY5iPbsFOaTpRyoNWOdcJxhBymWF2bbMdAMbMNe/hQ7UfIw5SeXnn04TL67g8mfwf+GCJVu7vCkaRQd8QHJS03HQKHRjturbYDSe9aJdmGbaeYQ1ymZa52MdPntKpkPfsbxpku+m6TP4ZMPi+pSP/XlqzpW8jbCQsg2Ibt04SLz9VPu6WoRa1FuXxY8mnJ1XvCkMhnKjM9puVa/jeKsmolbZeUKOZqD/lYvYH5yHrmoPMSHDgA6/1AjaBBoAArWiSLltAobNE4g9Y44a2BgzQIjTwMBSm6S7rVulAtEBljJTe+G7jRGrjrPHLTkIHOOeiwe2Vr9TAWHvhOGELNHQ3IjZPAndGao26gVXZoWzX8Ysk1cSmK30jPl6+xKrCIHaGVXBnZcOOc5J1rWn5ou4M72I4aXzTfU4o0/mLUu+DTNE9Drq4LU72y7v0Sy/eu1uX5GiL/tkZ2N/kvlKoSOYRYtvma8vl8Pv8PAAD//wMAz/peqCUEAAA=
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpRUy27cOBD8FYFnUeZLlKibMU6wA2/Wg2SQDXKj2K2YsEwNSMqJYfjfF9TMwvFpsReCaLGK1dVFvZBgH5EM5Pp0+vCEISdSk+y3kmBCU6Ypa4+8HaQeWtNIJZQS6jupib/FZzKQTjnHnUEqUSNVwFo6yslRADSccWCiVYXT/khkeCHWN4BP3mGzpK8Yk18CGcjfPsDys9xtfePmZYUmLjPuQ8o2uCLmr/3ulv55fTjeHc6nEqYCbjyQgTDgjHMQFDhTVEnO6aixpSBda10vtGTdGeZDxhjs3CR4eLsflhwwu0E0QjaMCkNeawI22yJ5tAmPz6eiYrPoptTrrbxthxfyhJEMov7XzDPdlZv91WMaVz/D1XnNNj2kbZ2sy0t8JjU5xeWEMXvc/LlOCR/H+flo08PH85myTR9+oVszwm5ZQyYD4dKQmmwGzbPNfgnVReNu9hjy9jFHH5J3/8UlSU3uvlSH2eZpiY/v5vHlbPP+f7j8ybp7H7Da3xQMRwGyRzoJrqnqwVLbSkO1lnpC7kwnOKnJbgnZh3VZU7UPGX/ErSUykI92TnjWdx3dvc/o8hpLn990ydUhLrC6XL3NkrOGNZKxAlrzac3VZwQf0WUsTRzjeuH7+h4itNhAuzVGDLk62Hxf/WHTPRmIsb3j1lhjrXZuwpbhxIBZzTpADRpBCehAGQVaGea0siNXYFs+TqVCaoIlOtWWVt6P0qLsKBN9S1VnOO07N1HdOt0rw1ppiydvPlZ3c8FZ1zOFIKnBCagy3FKjpKQgW9sJPXWdmH6z5JIHzgoZ4NP53W0KNECPoDkVirdUmaLAtB0du340o+6xdUXzLcaA829GffIuLmmZcnVJSPXOus9rKP+OakvLTx/or21GN4t7wFiVGVsfylO5jPX19fX1HwAAAP//AwD4jl43ggQAAA==
|
||||
@ -0,0 +1,5 @@
|
||||
https://dc.services.visualstudio.com/v2/track
|
||||
Content-Type:application/x-json-stream
|
||||
Content-Encoding:gzip
|
||||
|
||||
H4sIAAAAAAAACpRTS2/cNhD+KwLPosy3RN2MdYou3MSLZJEWvVGcUUxYphYU5SQw9r8H1G6QuLdeCGLAb+Z7DF9JdM9IenJ7Or17wZgXUpMctpJgwlBmKNNHrntpem0bqbRpefsvqUm4x++kJ63ynnuLVKJBqoBpOsjRUwC0nHFgQqvS031ZSP9KXGgAX4LHZl4+Y1rCHElP/g4R5q9ltguNn+YVmjRPuI9LdtEXMh/2u3v61+3h+HC4vFpwKeAmAOkJA844B0GBM0WV5JwOBjUF6bXznTCStRdYiBlTdFOzwNOv+TDniNn3ohGyYVRYcq4JuOwK5cEtePx+Kiw2i+5Kvd7K27V/JS+YSC/qn2Ze2t34Kdw8L8MaJri5nNktT0XlKc0nTDng5skmc5pcDnOsrpN2U8CYSU0ePlWHyeVxTs9vjPp00b//H/KPZfq7b+jXjLCb15hJT7gSpCbvnX8MEav9XWnHUYDskI6CG6o6cNRpaakx0ozIvW0FJzXZzTGHuM7rUu1jxi9pE0B68oebFrxQv03+MWT0eU1F1T+m7MIhzbD6XP3yn7OGNZKxAlrzac3VR4SQ0Gcs+o5pvfb7D0QYsYF2a0oYc3Vw+bH60y2PpCfWdZ4766xzxvsRNcORAXOGtYAGDIIS0IKyCoyyzBvlBq7AaT6MpUJqgiXuatswZ0SHWkuqBzZSpe1IreSOdpYJdNYh9/6Nj9XDtOF8xxSCpBZHoMpyR62SkoLUrhVmbFsx/mbJNX3OisGAL5e/sjEwAB2C4VQorqmyltPO6pYObTfYwXSofeF8jyni9JtR74NP8zKPubouT/XGuo9rLP+92hbpa4j025bR3eyfMFUlYxdiWe9rrOfz+fwDAAD//wMA+zzinzYEAAA=
|
||||
@ -3,7 +3,10 @@ using PlotLine.Services;
|
||||
|
||||
namespace PlotLine.Controllers;
|
||||
|
||||
public sealed class ProjectRestorePointsController(IProjectRestorePointService restorePoints, ILogger<ProjectRestorePointsController> logger) : Controller
|
||||
public sealed class ProjectRestorePointsController(
|
||||
IProjectRestorePointService restorePoints,
|
||||
IProjectRestoreService projectRestore,
|
||||
ILogger<ProjectRestorePointsController> logger) : Controller
|
||||
{
|
||||
public async Task<IActionResult> Index(int projectId)
|
||||
{
|
||||
@ -51,4 +54,22 @@ public sealed class ProjectRestorePointsController(IProjectRestorePointService r
|
||||
return RedirectToAction(nameof(Index), new { projectId });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Restore(int projectId, int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await projectRestore.RestoreAsync(projectId, id);
|
||||
TempData["ProjectRestorePointMessage"] = result.Message;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Project restore failed unexpectedly for restore point {ProjectRestorePointID} in project {ProjectID}.", id, projectId);
|
||||
TempData["ProjectRestorePointMessage"] = "Restore failed. No project data was changed.";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Index), new { projectId });
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ public interface IProjectRestorePointRepository
|
||||
Task<IReadOnlyList<ProjectRestorePoint>> ListAsync(int projectId);
|
||||
Task<ProjectRestorePoint?> GetAsync(int projectRestorePointId, int projectId);
|
||||
Task<ProjectRestorePoint> CreateAsync(int projectId, string restorePointName, string restorePointType, string jsonData, string? notes);
|
||||
Task RestoreAsync(int projectId, int sourceRestorePointId, int safetyRestorePointId, string jsonData);
|
||||
}
|
||||
|
||||
public sealed class ProjectRestorePointRepository(ISqlConnectionFactory connectionFactory) : IProjectRestorePointRepository
|
||||
@ -47,4 +48,20 @@ public sealed class ProjectRestorePointRepository(ISqlConnectionFactory connecti
|
||||
},
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task RestoreAsync(int projectId, int sourceRestorePointId, int safetyRestorePointId, string jsonData)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
await connection.ExecuteAsync(
|
||||
"dbo.ProjectRestore_ApplyFromJson",
|
||||
new
|
||||
{
|
||||
ProjectID = projectId,
|
||||
SourceRestorePointID = sourceRestorePointId,
|
||||
SafetyRestorePointID = safetyRestorePointId,
|
||||
JsonData = jsonData
|
||||
},
|
||||
commandType: CommandType.StoredProcedure,
|
||||
commandTimeout: 180);
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,3 +19,10 @@ public sealed class ProjectRestorePointDownload
|
||||
public string ContentType { get; init; } = "application/json";
|
||||
public string Json { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class ProjectRestoreResult
|
||||
{
|
||||
public bool Succeeded { get; init; }
|
||||
public string Message { get; init; } = string.Empty;
|
||||
public ProjectRestorePoint? SafetyBackup { get; init; }
|
||||
}
|
||||
|
||||
@ -62,6 +62,7 @@ public class Program
|
||||
builder.Services.AddScoped<IExportService, ExportService>();
|
||||
builder.Services.AddScoped<IProjectExportService, ProjectExportService>();
|
||||
builder.Services.AddScoped<IProjectRestorePointService, ProjectRestorePointService>();
|
||||
builder.Services.AddScoped<IProjectRestoreService, ProjectRestoreService>();
|
||||
builder.Services.AddScoped<IDynamicsService, DynamicsService>();
|
||||
builder.Services.AddScoped<IScenarioService, ScenarioService>();
|
||||
builder.Services.AddScoped<IArchiveService, ArchiveService>();
|
||||
|
||||
@ -8,6 +8,7 @@ public interface IProjectRestorePointService
|
||||
{
|
||||
Task<ProjectRestorePointListViewModel?> GetAsync(int projectId);
|
||||
Task<ProjectRestorePoint?> CreateManualAsync(int projectId, string? restorePointName, string? notes);
|
||||
Task<ProjectRestorePoint?> CreatePreRestoreAsync(int projectId, string sourceRestorePointName);
|
||||
Task<ProjectRestorePointDownload?> DownloadAsync(int projectRestorePointId, int projectId);
|
||||
}
|
||||
|
||||
@ -18,6 +19,7 @@ public sealed class ProjectRestorePointService(
|
||||
ILogger<ProjectRestorePointService> logger) : IProjectRestorePointService
|
||||
{
|
||||
private const string ManualRestorePointType = "Manual";
|
||||
private const string PreRestorePointType = "PreRestore";
|
||||
|
||||
public async Task<ProjectRestorePointListViewModel?> GetAsync(int projectId)
|
||||
{
|
||||
@ -36,19 +38,32 @@ public sealed class ProjectRestorePointService(
|
||||
|
||||
public async Task<ProjectRestorePoint?> CreateManualAsync(int projectId, string? restorePointName, string? notes)
|
||||
{
|
||||
var export = await projectExports.ExportAsync(projectId);
|
||||
if (export is null)
|
||||
{
|
||||
logger.LogWarning("Manual restore point requested for missing project {ProjectID}.", projectId);
|
||||
return null;
|
||||
}
|
||||
|
||||
var name = string.IsNullOrWhiteSpace(restorePointName)
|
||||
? $"Manual restore point - {DateTime.Now:yyyy-MM-dd HH:mm}"
|
||||
: restorePointName.Trim();
|
||||
|
||||
var created = await restorePoints.CreateAsync(projectId, name, ManualRestorePointType, export.Json, Clean(notes));
|
||||
logger.LogInformation("Manual restore point {ProjectRestorePointID} created for project {ProjectID} with {JsonSizeBytes} JSON bytes.",
|
||||
return await CreateAsync(projectId, name, ManualRestorePointType, Clean(notes));
|
||||
}
|
||||
|
||||
public async Task<ProjectRestorePoint?> CreatePreRestoreAsync(int projectId, string sourceRestorePointName)
|
||||
{
|
||||
var name = $"Automatic Pre-Restore Backup - {DateTime.Now:yyyy-MM-dd HH:mm}";
|
||||
var notes = $"Created automatically before restoring from \"{sourceRestorePointName}\".";
|
||||
return await CreateAsync(projectId, name, PreRestorePointType, notes);
|
||||
}
|
||||
|
||||
private async Task<ProjectRestorePoint?> CreateAsync(int projectId, string restorePointName, string restorePointType, string? notes)
|
||||
{
|
||||
var export = await projectExports.ExportAsync(projectId);
|
||||
if (export is null)
|
||||
{
|
||||
logger.LogWarning("{RestorePointType} restore point requested for missing project {ProjectID}.", restorePointType, projectId);
|
||||
return null;
|
||||
}
|
||||
|
||||
var created = await restorePoints.CreateAsync(projectId, restorePointName, restorePointType, export.Json, notes);
|
||||
logger.LogInformation("{RestorePointType} restore point {ProjectRestorePointID} created for project {ProjectID} with {JsonSizeBytes} JSON bytes.",
|
||||
restorePointType,
|
||||
created.ProjectRestorePointID,
|
||||
projectId,
|
||||
created.JsonSizeBytes);
|
||||
|
||||
123
PlotLine/Services/ProjectRestoreService.cs
Normal file
123
PlotLine/Services/ProjectRestoreService.cs
Normal file
@ -0,0 +1,123 @@
|
||||
using System.Text.Json;
|
||||
using PlotLine.Data;
|
||||
using PlotLine.Models;
|
||||
|
||||
namespace PlotLine.Services;
|
||||
|
||||
public interface IProjectRestoreService
|
||||
{
|
||||
Task<ProjectRestoreResult> RestoreAsync(int projectId, int projectRestorePointId);
|
||||
}
|
||||
|
||||
public sealed class ProjectRestoreService(
|
||||
IProjectRestorePointRepository restorePointRepository,
|
||||
IProjectRestorePointService restorePointService,
|
||||
ILogger<ProjectRestoreService> logger) : IProjectRestoreService
|
||||
{
|
||||
private static readonly JsonSerializerOptions ReadOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||
AllowTrailingCommas = true
|
||||
};
|
||||
|
||||
private static readonly JsonSerializerOptions RestoreSqlOptions = new()
|
||||
{
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
public async Task<ProjectRestoreResult> RestoreAsync(int projectId, int projectRestorePointId)
|
||||
{
|
||||
var restorePoint = await restorePointRepository.GetAsync(projectRestorePointId, projectId);
|
||||
if (restorePoint?.JsonData is null)
|
||||
{
|
||||
return Failure("Restore point could not be found.");
|
||||
}
|
||||
|
||||
ProjectBackupExport? backup;
|
||||
try
|
||||
{
|
||||
backup = JsonSerializer.Deserialize<ProjectBackupExport>(restorePoint.JsonData, ReadOptions);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Restore point {ProjectRestorePointID} contains invalid JSON.", projectRestorePointId);
|
||||
return Failure("Restore point JSON is invalid. No project data was changed.");
|
||||
}
|
||||
|
||||
var validationMessage = ValidateBackup(projectId, backup);
|
||||
if (validationMessage is not null)
|
||||
{
|
||||
return Failure(validationMessage);
|
||||
}
|
||||
|
||||
var safetyBackup = await restorePointService.CreatePreRestoreAsync(projectId, restorePoint.RestorePointName);
|
||||
if (safetyBackup is null)
|
||||
{
|
||||
logger.LogError("PreRestore backup creation failed for project {ProjectID}. Restore was aborted.", projectId);
|
||||
return Failure("A safety backup could not be created. Restore was aborted and no project data was changed.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var restoreJson = JsonSerializer.Serialize(backup, RestoreSqlOptions);
|
||||
await restorePointRepository.RestoreAsync(projectId, projectRestorePointId, safetyBackup.ProjectRestorePointID, restoreJson);
|
||||
logger.LogInformation("Project {ProjectID} restored from restore point {ProjectRestorePointID}. Safety backup {SafetyRestorePointID} was created first.",
|
||||
projectId,
|
||||
projectRestorePointId,
|
||||
safetyBackup.ProjectRestorePointID);
|
||||
|
||||
return new ProjectRestoreResult
|
||||
{
|
||||
Succeeded = true,
|
||||
Message = "Project restored. A safety backup of the previous state was created first.",
|
||||
SafetyBackup = safetyBackup
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Project restore failed for project {ProjectID} from restore point {ProjectRestorePointID}. Safety backup {SafetyRestorePointID} was retained.",
|
||||
projectId,
|
||||
projectRestorePointId,
|
||||
safetyBackup.ProjectRestorePointID);
|
||||
|
||||
return new ProjectRestoreResult
|
||||
{
|
||||
Succeeded = false,
|
||||
Message = "Restore failed. The safety backup was created, and the project data was left unchanged.",
|
||||
SafetyBackup = safetyBackup
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static string? ValidateBackup(int projectId, ProjectBackupExport? backup)
|
||||
{
|
||||
if (backup is null)
|
||||
{
|
||||
return "Restore point JSON is invalid. No project data was changed.";
|
||||
}
|
||||
|
||||
if (!string.Equals(backup.Format, "PlotWeaver.ProjectBackup", StringComparison.Ordinal))
|
||||
{
|
||||
return "Restore point format is not recognised. No project data was changed.";
|
||||
}
|
||||
|
||||
if (!string.Equals(backup.FormatVersion, "1.0", StringComparison.Ordinal))
|
||||
{
|
||||
return "Restore point format version is not supported. No project data was changed.";
|
||||
}
|
||||
|
||||
if (backup.Project?.ProjectID != projectId)
|
||||
{
|
||||
return "This restore point does not belong to the selected project. No project data was changed.";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static ProjectRestoreResult Failure(string message) => new()
|
||||
{
|
||||
Succeeded = false,
|
||||
Message = message
|
||||
};
|
||||
}
|
||||
306
PlotLine/Sql/036_ProjectRestoreApply.sql
Normal file
306
PlotLine/Sql/036_ProjectRestoreApply.sql
Normal file
@ -0,0 +1,306 @@
|
||||
SET ANSI_NULLS ON;
|
||||
GO
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.ProjectRestoreHistory', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.ProjectRestoreHistory
|
||||
(
|
||||
ProjectRestoreHistoryID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_ProjectRestoreHistory PRIMARY KEY,
|
||||
ProjectID int NOT NULL,
|
||||
SourceRestorePointID int NOT NULL,
|
||||
SafetyRestorePointID int NOT NULL,
|
||||
RestoredDateUtc datetime2 NOT NULL CONSTRAINT DF_ProjectRestoreHistory_RestoredDateUtc DEFAULT SYSUTCDATETIME(),
|
||||
PerformedBy nvarchar(100) NULL,
|
||||
Notes nvarchar(max) NULL,
|
||||
CONSTRAINT FK_ProjectRestoreHistory_Projects FOREIGN KEY(ProjectID) REFERENCES dbo.Projects(ProjectID),
|
||||
CONSTRAINT FK_ProjectRestoreHistory_SourceRestorePoint FOREIGN KEY(SourceRestorePointID) REFERENCES dbo.ProjectRestorePoints(ProjectRestorePointID),
|
||||
CONSTRAINT FK_ProjectRestoreHistory_SafetyRestorePoint FOREIGN KEY(SafetyRestorePointID) REFERENCES dbo.ProjectRestorePoints(ProjectRestorePointID)
|
||||
);
|
||||
END;
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_ProjectRestoreHistory_ProjectDate' AND object_id = OBJECT_ID(N'dbo.ProjectRestoreHistory'))
|
||||
CREATE INDEX IX_ProjectRestoreHistory_ProjectDate ON dbo.ProjectRestoreHistory(ProjectID, RestoredDateUtc DESC);
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.ProjectRestore_InsertJsonTable
|
||||
@TableName sysname,
|
||||
@JsonPath nvarchar(400),
|
||||
@JsonData nvarchar(max),
|
||||
@ProjectID int,
|
||||
@ProjectScopedOnly bit = 0
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
DECLARE @FullTableName nvarchar(300) = N'dbo.' + QUOTENAME(@TableName);
|
||||
DECLARE @ObjectID int = OBJECT_ID(@FullTableName, N'U');
|
||||
IF @ObjectID IS NULL
|
||||
THROW 51000, 'Restore table not found.', 1;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM OPENJSON(@JsonData, @JsonPath))
|
||||
RETURN;
|
||||
|
||||
DECLARE @ColumnList nvarchar(max);
|
||||
DECLARE @WithList nvarchar(max);
|
||||
DECLARE @SelectList nvarchar(max);
|
||||
DECLARE @ProjectFilter nvarchar(max) = N'';
|
||||
DECLARE @HasIdentity bit = 0;
|
||||
|
||||
DECLARE @Columns table
|
||||
(
|
||||
Name sysname NOT NULL,
|
||||
ColumnID int NOT NULL,
|
||||
IsIdentity bit NOT NULL,
|
||||
TypeName nvarchar(100) NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO @Columns (Name, ColumnID, IsIdentity, TypeName)
|
||||
SELECT
|
||||
c.name,
|
||||
c.column_id,
|
||||
c.is_identity,
|
||||
CASE
|
||||
WHEN t.name IN (N'nvarchar', N'nchar') AND c.max_length = -1 THEN t.name + N'(max)'
|
||||
WHEN t.name IN (N'nvarchar', N'nchar') THEN t.name + N'(' + CONVERT(nvarchar(20), c.max_length / 2) + N')'
|
||||
WHEN t.name IN (N'varchar', N'char', N'varbinary', N'binary') AND c.max_length = -1 THEN t.name + N'(max)'
|
||||
WHEN t.name IN (N'varchar', N'char', N'varbinary', N'binary') THEN t.name + N'(' + CONVERT(nvarchar(20), c.max_length) + N')'
|
||||
WHEN t.name IN (N'decimal', N'numeric') THEN t.name + N'(' + CONVERT(nvarchar(20), c.precision) + N',' + CONVERT(nvarchar(20), c.scale) + N')'
|
||||
WHEN t.name IN (N'datetime2', N'time', N'datetimeoffset') THEN t.name + N'(' + CONVERT(nvarchar(20), c.scale) + N')'
|
||||
ELSE t.name
|
||||
END
|
||||
FROM sys.columns c
|
||||
INNER JOIN sys.types t ON t.user_type_id = c.user_type_id
|
||||
WHERE c.object_id = @ObjectID
|
||||
AND c.is_computed = 0
|
||||
AND t.name <> N'timestamp'
|
||||
AND EXISTS
|
||||
(
|
||||
SELECT 1
|
||||
FROM OPENJSON(@JsonData, @JsonPath) rowJson
|
||||
CROSS APPLY OPENJSON(rowJson.value) propertyJson
|
||||
WHERE propertyJson.[key] COLLATE DATABASE_DEFAULT = c.name COLLATE DATABASE_DEFAULT
|
||||
);
|
||||
|
||||
SELECT @ColumnList = STUFF((SELECT N', ' + QUOTENAME(Name) FROM @Columns ORDER BY ColumnID FOR XML PATH(N''), TYPE).value(N'.', N'nvarchar(max)'), 1, 2, N'');
|
||||
SELECT @WithList = STUFF((SELECT N', ' + QUOTENAME(Name) + N' ' + TypeName + N' ''$.' + Name + N'''' FROM @Columns ORDER BY ColumnID FOR XML PATH(N''), TYPE).value(N'.', N'nvarchar(max)'), 1, 2, N'');
|
||||
SELECT @SelectList = STUFF((SELECT N', src.' + QUOTENAME(Name) FROM @Columns ORDER BY ColumnID FOR XML PATH(N''), TYPE).value(N'.', N'nvarchar(max)'), 1, 2, N'');
|
||||
SELECT @HasIdentity = CONVERT(bit, MAX(CONVERT(int, IsIdentity))) FROM @Columns;
|
||||
|
||||
IF @ProjectScopedOnly = 1
|
||||
SET @ProjectFilter = N' WHERE src.ProjectID = @ProjectID';
|
||||
|
||||
DECLARE @Sql nvarchar(max) = N'';
|
||||
|
||||
IF @HasIdentity = 1
|
||||
SET @Sql += N'SET IDENTITY_INSERT dbo.' + QUOTENAME(@TableName) + N' ON;';
|
||||
|
||||
SET @Sql += N'
|
||||
INSERT INTO dbo.' + QUOTENAME(@TableName) + N' (' + @ColumnList + N')
|
||||
SELECT ' + @SelectList + N'
|
||||
FROM OPENJSON(@JsonData, @JsonPath)
|
||||
WITH (' + @WithList + N') AS src' + @ProjectFilter + N';';
|
||||
|
||||
IF @HasIdentity = 1
|
||||
SET @Sql += N'SET IDENTITY_INSERT dbo.' + QUOTENAME(@TableName) + N' OFF;';
|
||||
|
||||
EXEC sp_executesql
|
||||
@Sql,
|
||||
N'@JsonData nvarchar(max), @JsonPath nvarchar(400), @ProjectID int',
|
||||
@JsonData = @JsonData,
|
||||
@JsonPath = @JsonPath,
|
||||
@ProjectID = @ProjectID;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.ProjectRestore_ApplyFromJson
|
||||
@ProjectID int,
|
||||
@SourceRestorePointID int,
|
||||
@SafetyRestorePointID int,
|
||||
@JsonData nvarchar(max)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
SET XACT_ABORT ON;
|
||||
|
||||
IF ISJSON(@JsonData) <> 1
|
||||
THROW 51001, 'Restore JSON is invalid.', 1;
|
||||
|
||||
IF JSON_VALUE(@JsonData, '$.Format') <> N'PlotWeaver.ProjectBackup'
|
||||
THROW 51002, 'Restore JSON format is not recognised.', 1;
|
||||
|
||||
IF JSON_VALUE(@JsonData, '$.FormatVersion') <> N'1.0'
|
||||
THROW 51003, 'Restore JSON format version is not supported.', 1;
|
||||
|
||||
IF TRY_CONVERT(int, JSON_VALUE(@JsonData, '$.Project.ProjectID')) <> @ProjectID
|
||||
THROW 51004, 'Restore point does not belong to this project.', 1;
|
||||
|
||||
DECLARE @RestoreTables table (TableName sysname NOT NULL PRIMARY KEY);
|
||||
INSERT INTO @RestoreTables (TableName)
|
||||
VALUES
|
||||
(N'ProjectTimelineSettings'), (N'ProjectTimelineMetricSettings'), (N'TimelineViewPresets'),
|
||||
(N'SceneMetricTypes'), (N'SceneMetricValues'), (N'Books'), (N'Chapters'), (N'Scenes'),
|
||||
(N'ScenePurposes'), (N'SceneOutcomes'), (N'PlotLines'), (N'PlotThreads'), (N'ThreadEvents'),
|
||||
(N'AssetStates'), (N'StoryAssets'), (N'AssetEvents'), (N'AssetDependencies'),
|
||||
(N'AssetCustodyEvents'), (N'AssetCustodyEventCharacters'), (N'Characters'), (N'SceneCharacters'),
|
||||
(N'CharacterAttributeTypes'), (N'CharacterAttributeEvents'), (N'CharacterKnowledge'),
|
||||
(N'CharacterRelationships'), (N'RelationshipEvents'), (N'Locations'), (N'LocationRelationships'),
|
||||
(N'SceneAssetLocations'), (N'SceneDependencies'), (N'ValidationRuns'), (N'ContinuityWarnings'),
|
||||
(N'SceneNotes'), (N'SceneWorkflow'), (N'SceneChecklistItems'), (N'SceneAttachments'),
|
||||
(N'ChapterGoals'), (N'Scenarios'), (N'ScenarioSceneOrder'), (N'ScenarioValidationRuns'),
|
||||
(N'ScenarioWarnings');
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
DECLARE @TableName sysname;
|
||||
DECLARE DisableCursor CURSOR LOCAL FAST_FORWARD FOR SELECT TableName FROM @RestoreTables;
|
||||
OPEN DisableCursor;
|
||||
FETCH NEXT FROM DisableCursor INTO @TableName;
|
||||
WHILE @@FETCH_STATUS = 0
|
||||
BEGIN
|
||||
DECLARE @DisableSql nvarchar(max) = N'ALTER TABLE dbo.' + QUOTENAME(@TableName) + N' NOCHECK CONSTRAINT ALL;';
|
||||
EXEC (@DisableSql);
|
||||
FETCH NEXT FROM DisableCursor INTO @TableName;
|
||||
END
|
||||
CLOSE DisableCursor;
|
||||
DEALLOCATE DisableCursor;
|
||||
|
||||
DELETE sw FROM dbo.ScenarioWarnings sw INNER JOIN dbo.Scenarios sc ON sc.ScenarioID = sw.ScenarioID WHERE sc.ProjectID = @ProjectID;
|
||||
DELETE svr FROM dbo.ScenarioValidationRuns svr INNER JOIN dbo.Scenarios sc ON sc.ScenarioID = svr.ScenarioID WHERE sc.ProjectID = @ProjectID;
|
||||
DELETE sso FROM dbo.ScenarioSceneOrder sso INNER JOIN dbo.Scenarios sc ON sc.ScenarioID = sso.ScenarioID WHERE sc.ProjectID = @ProjectID;
|
||||
DELETE FROM dbo.Scenarios WHERE ProjectID = @ProjectID;
|
||||
|
||||
DELETE FROM dbo.ContinuityWarnings WHERE ProjectID = @ProjectID;
|
||||
DELETE FROM dbo.ValidationRuns WHERE ProjectID = @ProjectID;
|
||||
|
||||
DELETE sa FROM dbo.SceneAttachments sa INNER JOIN dbo.Scenes s ON s.SceneID = sa.SceneID INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID INNER JOIN dbo.Books b ON b.BookID = c.BookID WHERE b.ProjectID = @ProjectID;
|
||||
DELETE sci FROM dbo.SceneChecklistItems sci INNER JOIN dbo.Scenes s ON s.SceneID = sci.SceneID INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID INNER JOIN dbo.Books b ON b.BookID = c.BookID WHERE b.ProjectID = @ProjectID;
|
||||
DELETE sw FROM dbo.SceneWorkflow sw INNER JOIN dbo.Scenes s ON s.SceneID = sw.SceneID INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID INNER JOIN dbo.Books b ON b.BookID = c.BookID WHERE b.ProjectID = @ProjectID;
|
||||
DELETE sn FROM dbo.SceneNotes sn INNER JOIN dbo.Scenes s ON s.SceneID = sn.SceneID INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID INNER JOIN dbo.Books b ON b.BookID = c.BookID WHERE b.ProjectID = @ProjectID;
|
||||
DELETE cg FROM dbo.ChapterGoals cg INNER JOIN dbo.Chapters c ON c.ChapterID = cg.ChapterID INNER JOIN dbo.Books b ON b.BookID = c.BookID WHERE b.ProjectID = @ProjectID;
|
||||
|
||||
DELETE sd FROM dbo.SceneDependencies sd
|
||||
LEFT JOIN dbo.Scenes ss ON ss.SceneID = sd.SourceSceneID
|
||||
LEFT JOIN dbo.Chapters sc ON sc.ChapterID = ss.ChapterID
|
||||
LEFT JOIN dbo.Books sb ON sb.BookID = sc.BookID
|
||||
LEFT JOIN dbo.Scenes ts ON ts.SceneID = sd.TargetSceneID
|
||||
LEFT JOIN dbo.Chapters tc ON tc.ChapterID = ts.ChapterID
|
||||
LEFT JOIN dbo.Books tb ON tb.BookID = tc.BookID
|
||||
WHERE sd.ProjectID = @ProjectID OR sb.ProjectID = @ProjectID OR tb.ProjectID = @ProjectID;
|
||||
|
||||
DELETE re FROM dbo.RelationshipEvents re INNER JOIN dbo.CharacterRelationships cr ON cr.CharacterRelationshipID = re.CharacterRelationshipID WHERE cr.ProjectID = @ProjectID;
|
||||
DELETE FROM dbo.CharacterRelationships WHERE ProjectID = @ProjectID;
|
||||
DELETE ck FROM dbo.CharacterKnowledge ck INNER JOIN dbo.Characters ch ON ch.CharacterID = ck.CharacterID WHERE ch.ProjectID = @ProjectID;
|
||||
DELETE cae FROM dbo.CharacterAttributeEvents cae INNER JOIN dbo.Characters ch ON ch.CharacterID = cae.CharacterID WHERE ch.ProjectID = @ProjectID;
|
||||
DELETE sc FROM dbo.SceneCharacters sc INNER JOIN dbo.Characters ch ON ch.CharacterID = sc.CharacterID WHERE ch.ProjectID = @ProjectID;
|
||||
|
||||
DELETE acec FROM dbo.AssetCustodyEventCharacters acec INNER JOIN dbo.AssetCustodyEvents ace ON ace.AssetCustodyEventID = acec.AssetCustodyEventID INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ace.StoryAssetID WHERE sa.ProjectID = @ProjectID;
|
||||
DELETE ace FROM dbo.AssetCustodyEvents ace INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ace.StoryAssetID WHERE sa.ProjectID = @ProjectID;
|
||||
DELETE ad FROM dbo.AssetDependencies ad INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ad.SourceAssetID WHERE sa.ProjectID = @ProjectID;
|
||||
DELETE ae FROM dbo.AssetEvents ae INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ae.StoryAssetID WHERE sa.ProjectID = @ProjectID;
|
||||
DELETE sal FROM dbo.SceneAssetLocations sal INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = sal.StoryAssetID WHERE sa.ProjectID = @ProjectID;
|
||||
DELETE FROM dbo.StoryAssets WHERE ProjectID = @ProjectID;
|
||||
|
||||
DELETE lr FROM dbo.LocationRelationships lr INNER JOIN dbo.Locations l ON l.LocationID = lr.FromLocationID WHERE l.ProjectID = @ProjectID;
|
||||
DELETE FROM dbo.Locations WHERE ProjectID = @ProjectID;
|
||||
|
||||
DELETE te FROM dbo.ThreadEvents te INNER JOIN dbo.PlotThreads pt ON pt.PlotThreadID = te.PlotThreadID INNER JOIN dbo.PlotLines pl ON pl.PlotLineID = pt.PlotLineID WHERE pl.ProjectID = @ProjectID;
|
||||
DELETE pt FROM dbo.PlotThreads pt INNER JOIN dbo.PlotLines pl ON pl.PlotLineID = pt.PlotLineID WHERE pl.ProjectID = @ProjectID;
|
||||
DELETE FROM dbo.PlotLines WHERE ProjectID = @ProjectID;
|
||||
|
||||
DELETE FROM dbo.TimelineViewPresets WHERE ProjectID = @ProjectID;
|
||||
DELETE FROM dbo.ProjectTimelineMetricSettings WHERE ProjectID = @ProjectID;
|
||||
DELETE FROM dbo.ProjectTimelineSettings WHERE ProjectID = @ProjectID;
|
||||
|
||||
DELETE smv FROM dbo.SceneMetricValues smv INNER JOIN dbo.Scenes s ON s.SceneID = smv.SceneID INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID INNER JOIN dbo.Books b ON b.BookID = c.BookID WHERE b.ProjectID = @ProjectID;
|
||||
DELETE FROM dbo.SceneMetricTypes WHERE ProjectID = @ProjectID;
|
||||
DELETE FROM dbo.CharacterAttributeTypes WHERE ProjectID = @ProjectID;
|
||||
DELETE FROM dbo.AssetStates WHERE ProjectID = @ProjectID;
|
||||
|
||||
DELETE sp FROM dbo.ScenePurposes sp INNER JOIN dbo.Scenes s ON s.SceneID = sp.SceneID INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID INNER JOIN dbo.Books b ON b.BookID = c.BookID WHERE b.ProjectID = @ProjectID;
|
||||
DELETE so FROM dbo.SceneOutcomes so INNER JOIN dbo.Scenes s ON s.SceneID = so.SceneID INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID INNER JOIN dbo.Books b ON b.BookID = c.BookID WHERE b.ProjectID = @ProjectID;
|
||||
DELETE s FROM dbo.Scenes s INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID INNER JOIN dbo.Books b ON b.BookID = c.BookID WHERE b.ProjectID = @ProjectID;
|
||||
DELETE c FROM dbo.Chapters c INNER JOIN dbo.Books b ON b.BookID = c.BookID WHERE b.ProjectID = @ProjectID;
|
||||
DELETE FROM dbo.Books WHERE ProjectID = @ProjectID;
|
||||
DELETE FROM dbo.Characters WHERE ProjectID = @ProjectID;
|
||||
|
||||
UPDATE p
|
||||
SET ProjectName = src.ProjectName,
|
||||
Description = src.Description,
|
||||
CreatedDate = src.CreatedDate,
|
||||
UpdatedDate = SYSUTCDATETIME(),
|
||||
IsArchived = src.IsArchived
|
||||
FROM dbo.Projects p
|
||||
CROSS APPLY OPENJSON(@JsonData, '$.Project')
|
||||
WITH
|
||||
(
|
||||
ProjectID int '$.ProjectID',
|
||||
ProjectName nvarchar(200) '$.ProjectName',
|
||||
Description nvarchar(max) '$.Description',
|
||||
CreatedDate datetime2 '$.CreatedDate',
|
||||
IsArchived bit '$.IsArchived'
|
||||
) src
|
||||
WHERE p.ProjectID = @ProjectID AND src.ProjectID = @ProjectID;
|
||||
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'SceneMetricTypes', N'$.Data.SceneMetricTypes', @JsonData, @ProjectID, 1;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'AssetStates', N'$.Data.AssetStates', @JsonData, @ProjectID, 1;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'CharacterAttributeTypes', N'$.Data.CharacterAttributeTypes', @JsonData, @ProjectID, 1;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'Books', N'$.Data.Books', @JsonData, @ProjectID, 0;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'Chapters', N'$.Data.Chapters', @JsonData, @ProjectID, 0;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'Characters', N'$.Data.Characters', @JsonData, @ProjectID, 0;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'Locations', N'$.Data.Locations', @JsonData, @ProjectID, 0;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'Scenes', N'$.Data.Scenes', @JsonData, @ProjectID, 0;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'ScenePurposes', N'$.Data.ScenePurposes', @JsonData, @ProjectID, 0;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'SceneOutcomes', N'$.Data.SceneOutcomes', @JsonData, @ProjectID, 0;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'SceneMetricValues', N'$.Data.SceneMetricValues', @JsonData, @ProjectID, 0;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'ProjectTimelineSettings', N'$.Data.TimelineSettings', @JsonData, @ProjectID, 0;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'ProjectTimelineMetricSettings', N'$.Data.TimelineMetricSettings', @JsonData, @ProjectID, 0;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'TimelineViewPresets', N'$.Data.TimelineViewPresets', @JsonData, @ProjectID, 0;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'PlotLines', N'$.Data.PlotLines', @JsonData, @ProjectID, 0;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'PlotThreads', N'$.Data.PlotThreads', @JsonData, @ProjectID, 0;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'ThreadEvents', N'$.Data.ThreadEvents', @JsonData, @ProjectID, 0;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'StoryAssets', N'$.Data.StoryAssets', @JsonData, @ProjectID, 0;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'AssetEvents', N'$.Data.AssetEvents', @JsonData, @ProjectID, 0;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'AssetDependencies', N'$.Data.AssetDependencies', @JsonData, @ProjectID, 0;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'AssetCustodyEvents', N'$.Data.AssetCustodyEvents', @JsonData, @ProjectID, 0;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'AssetCustodyEventCharacters', N'$.Data.AssetCustodyCharacters', @JsonData, @ProjectID, 0;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'SceneCharacters', N'$.Data.SceneCharacters', @JsonData, @ProjectID, 0;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'CharacterAttributeEvents', N'$.Data.CharacterAttributeEvents', @JsonData, @ProjectID, 0;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'CharacterKnowledge', N'$.Data.CharacterKnowledge', @JsonData, @ProjectID, 0;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'CharacterRelationships', N'$.Data.CharacterRelationships', @JsonData, @ProjectID, 0;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'RelationshipEvents', N'$.Data.RelationshipEvents', @JsonData, @ProjectID, 0;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'LocationRelationships', N'$.Data.LocationRelationships', @JsonData, @ProjectID, 0;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'SceneAssetLocations', N'$.Data.SceneAssetLocations', @JsonData, @ProjectID, 0;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'SceneDependencies', N'$.Data.SceneDependencies', @JsonData, @ProjectID, 0;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'ValidationRuns', N'$.Data.ValidationRuns', @JsonData, @ProjectID, 0;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'ContinuityWarnings', N'$.Data.ContinuityWarnings', @JsonData, @ProjectID, 0;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'SceneNotes', N'$.Data.SceneNotes', @JsonData, @ProjectID, 0;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'SceneWorkflow', N'$.Data.SceneWorkflow', @JsonData, @ProjectID, 0;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'SceneChecklistItems', N'$.Data.SceneChecklistItems', @JsonData, @ProjectID, 0;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'SceneAttachments', N'$.Data.SceneAttachments', @JsonData, @ProjectID, 0;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'ChapterGoals', N'$.Data.ChapterGoals', @JsonData, @ProjectID, 0;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'Scenarios', N'$.Data.Scenarios', @JsonData, @ProjectID, 0;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'ScenarioSceneOrder', N'$.Data.ScenarioSceneOrder', @JsonData, @ProjectID, 0;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'ScenarioValidationRuns', N'$.Data.ScenarioValidationRuns', @JsonData, @ProjectID, 0;
|
||||
EXEC dbo.ProjectRestore_InsertJsonTable N'ScenarioWarnings', N'$.Data.ScenarioWarnings', @JsonData, @ProjectID, 0;
|
||||
|
||||
DECLARE EnableCursor CURSOR LOCAL FAST_FORWARD FOR SELECT TableName FROM @RestoreTables;
|
||||
OPEN EnableCursor;
|
||||
FETCH NEXT FROM EnableCursor INTO @TableName;
|
||||
WHILE @@FETCH_STATUS = 0
|
||||
BEGIN
|
||||
DECLARE @EnableSql nvarchar(max) = N'ALTER TABLE dbo.' + QUOTENAME(@TableName) + N' WITH CHECK CHECK CONSTRAINT ALL;';
|
||||
EXEC (@EnableSql);
|
||||
FETCH NEXT FROM EnableCursor INTO @TableName;
|
||||
END
|
||||
CLOSE EnableCursor;
|
||||
DEALLOCATE EnableCursor;
|
||||
|
||||
INSERT INTO dbo.ProjectRestoreHistory (ProjectID, SourceRestorePointID, SafetyRestorePointID, PerformedBy, Notes)
|
||||
VALUES (@ProjectID, @SourceRestorePointID, @SafetyRestorePointID, SUSER_SNAME(), N'Project restored from saved restore point.');
|
||||
|
||||
COMMIT TRANSACTION;
|
||||
END;
|
||||
GO
|
||||
@ -97,11 +97,39 @@
|
||||
<td>@restorePoint.Notes</td>
|
||||
<td class="text-end">
|
||||
<a class="btn btn-outline-secondary btn-sm" asp-action="Download" asp-route-projectId="@Model.Project.ProjectID" asp-route-id="@restorePoint.ProjectRestorePointID">Download</a>
|
||||
<button class="btn btn-outline-danger btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#restorePointModal-@restorePoint.ProjectRestorePointID">Restore</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@foreach (var restorePoint in Model.RestorePoints)
|
||||
{
|
||||
<div class="modal fade" id="restorePointModal-@restorePoint.ProjectRestorePointID" tabindex="-1" aria-labelledby="restorePointModalLabel-@restorePoint.ProjectRestorePointID" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title fs-5" id="restorePointModalLabel-@restorePoint.ProjectRestorePointID">Restore Project</h2>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>This will replace the current contents of this project with the selected restore point.</p>
|
||||
<p>A safety backup of the current project will be created automatically before the restore begins.</p>
|
||||
<p class="mb-0">This operation may overwrite recent changes.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<form asp-action="Restore" method="post" data-confirm-skip="true">
|
||||
<input type="hidden" name="projectId" value="@Model.Project.ProjectID" />
|
||||
<input type="hidden" name="id" value="@restorePoint.ProjectRestorePointID" />
|
||||
<button class="btn btn-danger" type="submit">Restore Project</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</section>
|
||||
|
||||
@ -99,7 +99,7 @@
|
||||
|
||||
document.addEventListener("submit", (event) => {
|
||||
const form = event.target;
|
||||
if (!(form instanceof HTMLFormElement) || form.dataset.confirmed === "true") {
|
||||
if (!(form instanceof HTMLFormElement) || form.dataset.confirmed === "true" || form.dataset.confirmSkip === "true") {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user