Skip to content

Commit

Permalink
refactor: migrated from JavaScript to TypeScript
Browse files Browse the repository at this point in the history
  • Loading branch information
thorsten committed Jan 20, 2025
1 parent 4803589 commit d94272b
Show file tree
Hide file tree
Showing 26 changed files with 675 additions and 574 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ This is a log of major user-visible changes in each phpMyFAQ release.
- added plugin administration backend (Thorsten)
- improved online update feature (Thorsten)
- migrated from WYSIWYG editor from TinyMCE to Jodit Editor (Thorsten)
- WIP: migrated from JavaScript to TypeScript (Thorsten)
- migrated from JavaScript to TypeScript (Thorsten)
- migrated from Webpack to Vite v6 (Thorsten)
- migrated from Jest to Vitest v3 (Thorsten)

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"@types/highlightjs": "^9.12.6",
"@types/masonry-layout": "^4.2.8",
"@types/node": "^20.17.14",
"@types/sortablejs": "^1.15.8",
"@vitest/coverage-v8": "2.1.5",
"autoprefixer": "^10.4.20",
"babel-preset-env": "^1.7.0",
Expand Down
1 change: 1 addition & 0 deletions phpmyfaq/admin/assets/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './attachment';
export * from './category';
export * from './faqs';
export * from './forms';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,25 @@

import { addElement } from '../../../../assets/src/utils';

export const handleAttachmentUploads = () => {
const filesToUpload = document.getElementById('filesToUpload');
const fileUploadButton = document.getElementById('pmf-attachment-modal-upload');
export const handleAttachmentUploads = (): void => {
const filesToUpload = document.getElementById('filesToUpload') as HTMLInputElement | null;
const fileUploadButton = document.getElementById('pmf-attachment-modal-upload') as HTMLButtonElement | null;

// Calculate upload size and show file to upload
if (filesToUpload) {
filesToUpload.addEventListener('change', function () {
const files = filesToUpload.files;
const fileSize = document.getElementById('filesize');
const fileList = document.querySelector('.pmf-attachment-upload-files');
const fileSize = document.getElementById('filesize') as HTMLElement;
const fileList = document.querySelector('.pmf-attachment-upload-files') as HTMLElement;

fileList.classList.remove('invisible');

let bytes = 0;
let numFiles = files.length;
let fileItems = [];
const numFiles = files?.length || 0;
const fileItems: HTMLElement[] = [];
for (let fileId = 0; fileId < numFiles; fileId++) {
bytes += files[fileId].size;
fileItems.push(addElement('li', { innerText: files[fileId].name }));
bytes += files![fileId].size;
fileItems.push(addElement('li', { innerText: files![fileId].name }));
}

let output = bytes + ' bytes';
Expand All @@ -50,18 +50,18 @@ export const handleAttachmentUploads = () => {
});

// handle upload button
fileUploadButton.addEventListener('click', async (event) => {
fileUploadButton?.addEventListener('click', async (event: MouseEvent) => {
event.preventDefault();
event.stopImmediatePropagation();

const files = filesToUpload.files;
const formData = new FormData();

for (let i = 0; i < files.length; i++) {
formData.append('filesToUpload[]', files[i]);
for (let i = 0; i < (files?.length || 0); i++) {
formData.append('filesToUpload[]', files![i]);
}
formData.append('record_id', document.getElementById('attachment_record_id').value);
formData.append('record_lang', document.getElementById('attachment_record_lang').value);
formData.append('record_id', (document.getElementById('attachment_record_id') as HTMLInputElement).value);
formData.append('record_lang', (document.getElementById('attachment_record_lang') as HTMLInputElement).value);

try {
const response = await fetch('./api/content/attachments/upload', {
Expand All @@ -72,18 +72,18 @@ export const handleAttachmentUploads = () => {

if (!response.ok) {
const error = new Error('Network response was not ok');
error.cause = { response };
(error as any).cause = { response };
throw error;
}

const attachments = await response.json();
const modal = document.getElementById('attachmentModal');
const modalBackdrop = document.querySelector('.modal-backdrop.fade.show');
const attachmentList = document.querySelector('.adminAttachments');
const fileSize = document.getElementById('filesize');
const modal = document.getElementById('attachmentModal') as HTMLElement;
const modalBackdrop = document.querySelector('.modal-backdrop.fade.show') as HTMLElement;
const attachmentList = document.querySelector('.adminAttachments') as HTMLElement;
const fileSize = document.getElementById('filesize') as HTMLElement;
const fileList = document.querySelectorAll('.pmf-attachment-upload-files li');

attachments.forEach((attachment) => {
attachments.forEach((attachment: { attachmentId: string; fileName: string }) => {
const csrfToken = attachmentList.getAttribute('data-pmf-csrf-token');
attachmentList.insertAdjacentElement(
'beforeend',
Expand Down Expand Up @@ -122,8 +122,8 @@ export const handleAttachmentUploads = () => {
modal.classList.remove('show');
modalBackdrop.remove();
} catch (error) {
if (error.cause && error.cause.response) {
const errors = await error.cause.response.json();
if ((error as any).cause && (error as any).cause.response) {
const errors = await (error as any).cause.response.json();
console.log(errors);
} else {
console.log('An error occurred:', error);
Expand Down
78 changes: 0 additions & 78 deletions phpmyfaq/admin/assets/src/content/attachments.js

This file was deleted.

83 changes: 83 additions & 0 deletions phpmyfaq/admin/assets/src/content/attachments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* Attachment administration stuff
*
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at https://mozilla.org/MPL/2.0/.
*
* @package phpMyFAQ
* @author Thorsten Rinne <thorsten@phpmyfaq.de>
* @copyright 2022-2025 phpMyFAQ Team
* @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
* @link https://www.phpmyfaq.de
* @since 2022-03-22
*/

import { deleteAttachments, refreshAttachments } from '../api';
import { pushErrorNotification, pushNotification } from '../../../../assets/src/utils';

export const handleDeleteAttachments = (): void => {
const deleteButtons = document.querySelectorAll<HTMLButtonElement>('.btn-delete-attachment');

if (deleteButtons.length > 0) {
deleteButtons.forEach((button) => {
const newButton = button.cloneNode(true) as HTMLButtonElement;
button.replaceWith(newButton);

newButton.addEventListener('click', async (event: MouseEvent) => {
event.preventDefault();

const attachmentId = newButton.getAttribute('data-attachment-id');
const csrf = newButton.getAttribute('data-csrf');

if (attachmentId && csrf) {
const response = await deleteAttachments(attachmentId, csrf);

if (response.success) {
pushNotification(response.success);
const row = document.getElementById(`attachment_${attachmentId}`) as HTMLElement;
row.style.opacity = '0';
row.addEventListener('transitionend', () => row.remove());
}
if (response.error) {
pushErrorNotification(response.error);
}
}
});
});
}
};

export const handleRefreshAttachments = (): void => {
const refreshButtons = document.querySelectorAll<HTMLButtonElement>('.btn-refresh-attachment');

if (refreshButtons.length > 0) {
refreshButtons.forEach((button) => {
const newButton = button.cloneNode(true) as HTMLButtonElement;
button.replaceWith(newButton);

newButton.addEventListener('click', async (event: MouseEvent) => {
event.preventDefault();

const attachmentId = newButton.getAttribute('data-attachment-id');
const csrf = newButton.getAttribute('data-csrf');

if (attachmentId && csrf) {
const response = await refreshAttachments(attachmentId, csrf);

if (response.success) {
pushNotification(response.success);
if (response.delete) {
const row = document.getElementById(`attachment_${attachmentId}`) as HTMLElement;
row.style.opacity = '0';
row.addEventListener('transitionend', () => row.remove());
}
}
if (response.error) {
pushErrorNotification(response.error);
}
}
});
});
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,31 @@
* @since 2014-06-02
*/

import Sortable from 'sortablejs';
import Sortable, { SortableEvent } from 'sortablejs';
import { deleteCategory, setCategoryTree } from '../api';
import { pushErrorNotification, pushNotification } from '../utils';
import { pushErrorNotification, pushNotification } from '../../../../assets/src/utils';

const nestedQuery = '.nested-sortable';
const identifier = 'pmfCatid';

export const handleCategories = () => {
const root = document.getElementById('pmf-category-tree');
const nestedSortables = document.querySelectorAll(nestedQuery);
interface SerializedTree {
id: string;
children: SerializedTree[];
}

export const handleCategories = (): void => {
const root = document.getElementById('pmf-category-tree') as HTMLElement;
const nestedSortables = document.querySelectorAll<HTMLElement>(nestedQuery);
for (let i = 0; i < nestedSortables.length; i++) {
new Sortable(nestedSortables[i], {
group: 'Categories',
animation: 150,
fallbackOnBody: true,
swapThreshold: 0.65,
dataIdAttr: identifier,
onEnd: async (event) => {
const categoryId = event.item.getAttribute('data-pmf-catid');
const csrf = document.querySelector('input[name=pmf-csrf-token]').value;
onEnd: async (event: SortableEvent) => {
const categoryId = event.item.getAttribute('data-pmf-catid') as string;
const csrf = (document.querySelector('input[name=pmf-csrf-token]') as HTMLInputElement).value;
const data = serializedTree(root);
const response = await setCategoryTree(data, categoryId, csrf);
if (response.success) {
Expand All @@ -44,33 +49,34 @@ export const handleCategories = () => {
});
}

const serializedTree = (sortable) => {
const serializedTree = (sortable: HTMLElement): SerializedTree[] => {
return Array.from(sortable.children).map((child) => {
const nested = child.querySelector(nestedQuery);
const nested = child.querySelector(nestedQuery) as HTMLElement;
return {
id: child.dataset[identifier],
id: child.dataset[identifier] as string,
children: nested ? serializedTree(nested) : [],
};
});
};
};

export const handleCategoryDelete = async () => {
export const handleCategoryDelete = async (): Promise<void> => {
const buttonDelete = document.getElementsByName('pmf-category-delete-button');

if (buttonDelete) {
buttonDelete.forEach((button) => {
button.addEventListener('click', async (event) => {
button.addEventListener('click', async (event: Event) => {
event.preventDefault();
const categoryId = event.target.getAttribute('data-pmf-category-id');
const language = event.target.getAttribute('data-pmf-language');
const csrfToken = document.querySelector('input[name=pmf-csrf-token]').value;
const target = event.target as HTMLElement;
const categoryId = target.getAttribute('data-pmf-category-id') as string;
const language = target.getAttribute('data-pmf-language') as string;
const csrfToken = (document.querySelector('input[name=pmf-csrf-token]') as HTMLInputElement).value;

const response = await deleteCategory(categoryId, language, csrfToken);
if (response.success) {
pushNotification(response.success);
}
document.getElementById(`pmf-category-${categoryId}`).remove();
document.getElementById(`pmf-category-${categoryId}`)?.remove();
});
});
}
Expand Down
Loading

0 comments on commit d94272b

Please sign in to comment.