admin.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. import './public-path';
  2. import { createRoot } from 'react-dom/client';
  3. import Rails from '@rails/ujs';
  4. import ready from '../mastodon/ready';
  5. const setAnnouncementEndsAttributes = (target: HTMLInputElement) => {
  6. const valid = target.value && target.validity.valid;
  7. const element = document.querySelector<HTMLInputElement>(
  8. 'input[type="datetime-local"]#announcement_ends_at',
  9. );
  10. if (!element) return;
  11. if (valid) {
  12. element.classList.remove('optional');
  13. element.required = true;
  14. element.min = target.value;
  15. } else {
  16. element.classList.add('optional');
  17. element.removeAttribute('required');
  18. element.removeAttribute('min');
  19. }
  20. };
  21. Rails.delegate(
  22. document,
  23. 'input[type="datetime-local"]#announcement_starts_at',
  24. 'change',
  25. ({ target }) => {
  26. if (target instanceof HTMLInputElement)
  27. setAnnouncementEndsAttributes(target);
  28. },
  29. );
  30. const batchCheckboxClassName = '.batch-checkbox input[type="checkbox"]';
  31. const showSelectAll = () => {
  32. const selectAllMatchingElement = document.querySelector(
  33. '.batch-table__select-all',
  34. );
  35. selectAllMatchingElement?.classList.add('active');
  36. };
  37. const hideSelectAll = () => {
  38. const selectAllMatchingElement = document.querySelector(
  39. '.batch-table__select-all',
  40. );
  41. const hiddenField = document.querySelector<HTMLInputElement>(
  42. 'input#select_all_matching',
  43. );
  44. const selectedMsg = document.querySelector(
  45. '.batch-table__select-all .selected',
  46. );
  47. const notSelectedMsg = document.querySelector(
  48. '.batch-table__select-all .not-selected',
  49. );
  50. selectAllMatchingElement?.classList.remove('active');
  51. selectedMsg?.classList.remove('active');
  52. notSelectedMsg?.classList.add('active');
  53. if (hiddenField) hiddenField.value = '0';
  54. };
  55. Rails.delegate(document, '#batch_checkbox_all', 'change', ({ target }) => {
  56. if (!(target instanceof HTMLInputElement)) return;
  57. const selectAllMatchingElement = document.querySelector(
  58. '.batch-table__select-all',
  59. );
  60. document
  61. .querySelectorAll<HTMLInputElement>(batchCheckboxClassName)
  62. .forEach((content) => {
  63. content.checked = target.checked;
  64. });
  65. if (selectAllMatchingElement) {
  66. if (target.checked) {
  67. showSelectAll();
  68. } else {
  69. hideSelectAll();
  70. }
  71. }
  72. });
  73. Rails.delegate(document, '.batch-table__select-all button', 'click', () => {
  74. const hiddenField = document.querySelector<HTMLInputElement>(
  75. '#select_all_matching',
  76. );
  77. if (!hiddenField) return;
  78. const active = hiddenField.value === '1';
  79. const selectedMsg = document.querySelector(
  80. '.batch-table__select-all .selected',
  81. );
  82. const notSelectedMsg = document.querySelector(
  83. '.batch-table__select-all .not-selected',
  84. );
  85. if (!selectedMsg || !notSelectedMsg) return;
  86. if (active) {
  87. hiddenField.value = '0';
  88. selectedMsg.classList.remove('active');
  89. notSelectedMsg.classList.add('active');
  90. } else {
  91. hiddenField.value = '1';
  92. notSelectedMsg.classList.remove('active');
  93. selectedMsg.classList.add('active');
  94. }
  95. });
  96. Rails.delegate(document, batchCheckboxClassName, 'change', () => {
  97. const checkAllElement = document.querySelector<HTMLInputElement>(
  98. 'input#batch_checkbox_all',
  99. );
  100. const selectAllMatchingElement = document.querySelector(
  101. '.batch-table__select-all',
  102. );
  103. if (checkAllElement) {
  104. const allCheckboxes = Array.from(
  105. document.querySelectorAll<HTMLInputElement>(batchCheckboxClassName),
  106. );
  107. checkAllElement.checked = allCheckboxes.every((content) => content.checked);
  108. checkAllElement.indeterminate =
  109. !checkAllElement.checked &&
  110. allCheckboxes.some((content) => content.checked);
  111. if (selectAllMatchingElement) {
  112. if (checkAllElement.checked) {
  113. showSelectAll();
  114. } else {
  115. hideSelectAll();
  116. }
  117. }
  118. }
  119. });
  120. Rails.delegate(
  121. document,
  122. '.filter-subset--with-select select',
  123. 'change',
  124. ({ target }) => {
  125. if (target instanceof HTMLSelectElement) target.form?.submit();
  126. },
  127. );
  128. const onDomainBlockSeverityChange = (target: HTMLSelectElement) => {
  129. const rejectMediaDiv = document.querySelector(
  130. '.input.with_label.domain_block_reject_media',
  131. );
  132. const rejectReportsDiv = document.querySelector(
  133. '.input.with_label.domain_block_reject_reports',
  134. );
  135. if (rejectMediaDiv && rejectMediaDiv instanceof HTMLElement) {
  136. rejectMediaDiv.style.display =
  137. target.value === 'suspend' ? 'none' : 'block';
  138. }
  139. if (rejectReportsDiv && rejectReportsDiv instanceof HTMLElement) {
  140. rejectReportsDiv.style.display =
  141. target.value === 'suspend' ? 'none' : 'block';
  142. }
  143. };
  144. Rails.delegate(document, '#domain_block_severity', 'change', ({ target }) => {
  145. if (target instanceof HTMLSelectElement) onDomainBlockSeverityChange(target);
  146. });
  147. const onEnableBootstrapTimelineAccountsChange = (target: HTMLInputElement) => {
  148. const bootstrapTimelineAccountsField =
  149. document.querySelector<HTMLInputElement>(
  150. '#form_admin_settings_bootstrap_timeline_accounts',
  151. );
  152. if (bootstrapTimelineAccountsField) {
  153. bootstrapTimelineAccountsField.disabled = !target.checked;
  154. if (target.checked) {
  155. bootstrapTimelineAccountsField.parentElement?.classList.remove(
  156. 'disabled',
  157. );
  158. bootstrapTimelineAccountsField.parentElement?.parentElement?.classList.remove(
  159. 'disabled',
  160. );
  161. } else {
  162. bootstrapTimelineAccountsField.parentElement?.classList.add('disabled');
  163. bootstrapTimelineAccountsField.parentElement?.parentElement?.classList.add(
  164. 'disabled',
  165. );
  166. }
  167. }
  168. };
  169. Rails.delegate(
  170. document,
  171. '#form_admin_settings_enable_bootstrap_timeline_accounts',
  172. 'change',
  173. ({ target }) => {
  174. if (target instanceof HTMLInputElement)
  175. onEnableBootstrapTimelineAccountsChange(target);
  176. },
  177. );
  178. const onChangeRegistrationMode = (target: HTMLSelectElement) => {
  179. const enabled = target.value === 'approved';
  180. document
  181. .querySelectorAll<HTMLElement>(
  182. '.form_admin_settings_registrations_mode .warning-hint',
  183. )
  184. .forEach((warning_hint) => {
  185. warning_hint.style.display = target.value === 'open' ? 'inline' : 'none';
  186. });
  187. document
  188. .querySelectorAll<HTMLInputElement>(
  189. 'input#form_admin_settings_require_invite_text',
  190. )
  191. .forEach((input) => {
  192. input.disabled = !enabled;
  193. if (enabled) {
  194. let element: HTMLElement | null = input;
  195. do {
  196. element.classList.remove('disabled');
  197. element = element.parentElement;
  198. } while (element && !element.classList.contains('fields-group'));
  199. } else {
  200. let element: HTMLElement | null = input;
  201. do {
  202. element.classList.add('disabled');
  203. element = element.parentElement;
  204. } while (element && !element.classList.contains('fields-group'));
  205. }
  206. });
  207. };
  208. const convertUTCDateTimeToLocal = (value: string) => {
  209. const date = new Date(value + 'Z');
  210. const twoChars = (x: number) => x.toString().padStart(2, '0');
  211. return `${date.getFullYear()}-${twoChars(date.getMonth() + 1)}-${twoChars(date.getDate())}T${twoChars(date.getHours())}:${twoChars(date.getMinutes())}`;
  212. };
  213. function convertLocalDatetimeToUTC(value: string) {
  214. const date = new Date(value);
  215. const fullISO8601 = date.toISOString();
  216. return fullISO8601.slice(0, fullISO8601.indexOf('T') + 6);
  217. }
  218. Rails.delegate(
  219. document,
  220. '#form_admin_settings_registrations_mode',
  221. 'change',
  222. ({ target }) => {
  223. if (target instanceof HTMLSelectElement) onChangeRegistrationMode(target);
  224. },
  225. );
  226. async function mountReactComponent(element: Element) {
  227. const componentName = element.getAttribute('data-admin-component');
  228. const stringProps = element.getAttribute('data-props');
  229. if (!stringProps) return;
  230. const componentProps = JSON.parse(stringProps) as object;
  231. const { default: AdminComponent } = await import(
  232. '@/mastodon/containers/admin_component'
  233. );
  234. const { default: Component } = (await import(
  235. `@/mastodon/components/admin/${componentName}`
  236. )) as { default: React.ComponentType };
  237. const root = createRoot(element);
  238. root.render(
  239. <AdminComponent>
  240. <Component {...componentProps} />
  241. </AdminComponent>,
  242. );
  243. }
  244. ready(() => {
  245. const domainBlockSeveritySelect = document.querySelector<HTMLSelectElement>(
  246. 'select#domain_block_severity',
  247. );
  248. if (domainBlockSeveritySelect)
  249. onDomainBlockSeverityChange(domainBlockSeveritySelect);
  250. const enableBootstrapTimelineAccounts =
  251. document.querySelector<HTMLInputElement>(
  252. 'input#form_admin_settings_enable_bootstrap_timeline_accounts',
  253. );
  254. if (enableBootstrapTimelineAccounts)
  255. onEnableBootstrapTimelineAccountsChange(enableBootstrapTimelineAccounts);
  256. const registrationMode = document.querySelector<HTMLSelectElement>(
  257. 'select#form_admin_settings_registrations_mode',
  258. );
  259. if (registrationMode) onChangeRegistrationMode(registrationMode);
  260. const checkAllElement = document.querySelector<HTMLInputElement>(
  261. 'input#batch_checkbox_all',
  262. );
  263. if (checkAllElement) {
  264. const allCheckboxes = Array.from(
  265. document.querySelectorAll<HTMLInputElement>(batchCheckboxClassName),
  266. );
  267. checkAllElement.checked = allCheckboxes.every((content) => content.checked);
  268. checkAllElement.indeterminate =
  269. !checkAllElement.checked &&
  270. allCheckboxes.some((content) => content.checked);
  271. }
  272. document
  273. .querySelector('a#add-instance-button')
  274. ?.addEventListener('click', (e) => {
  275. const domain = document.querySelector<HTMLInputElement>(
  276. 'input[type="text"]#by_domain',
  277. )?.value;
  278. if (domain && e.target instanceof HTMLAnchorElement) {
  279. const url = new URL(e.target.href);
  280. url.searchParams.set('_domain', domain);
  281. e.target.href = url.toString();
  282. }
  283. });
  284. document
  285. .querySelectorAll<HTMLInputElement>('input[type="datetime-local"]')
  286. .forEach((element) => {
  287. if (element.value) {
  288. element.value = convertUTCDateTimeToLocal(element.value);
  289. }
  290. if (element.placeholder) {
  291. element.placeholder = convertUTCDateTimeToLocal(element.placeholder);
  292. }
  293. });
  294. Rails.delegate(document, 'form', 'submit', ({ target }) => {
  295. if (target instanceof HTMLFormElement)
  296. target
  297. .querySelectorAll<HTMLInputElement>('input[type="datetime-local"]')
  298. .forEach((element) => {
  299. if (element.value && element.validity.valid) {
  300. element.value = convertLocalDatetimeToUTC(element.value);
  301. }
  302. });
  303. });
  304. const announcementStartsAt = document.querySelector<HTMLInputElement>(
  305. 'input[type="datetime-local"]#announcement_starts_at',
  306. );
  307. if (announcementStartsAt) {
  308. setAnnouncementEndsAttributes(announcementStartsAt);
  309. }
  310. document.querySelectorAll('[data-admin-component]').forEach((element) => {
  311. void mountReactComponent(element);
  312. });
  313. }).catch((reason: unknown) => {
  314. throw reason;
  315. });