admin_lsac 2.0.24 → 2.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,24 +4,22 @@ For configuring your own admin panel, follow the next instructions.
4
4
 
5
5
  ## How to install it?
6
6
 
7
- - `npm install admin_lsac`
7
+ The following command will download and install the specified package and its dependencies into your project's node_modules directory, making it available for use in your project.
8
8
 
9
- This command will download and install the specified package and its dependencies into your project's node_modules directory, making it available for use in your project.
9
+ - `npm install admin_lsac`
10
10
 
11
11
  ## What do you need to import?
12
12
 
13
+ You can import these in App.js / App.ts or wherever your routes to the pages are located.
14
+
13
15
  - `import { pageToUrl } from 'admin_lsac'`
14
16
 
15
17
  - `import { AdminPage } from 'admin_lsac'`
16
18
 
17
- You can import these in App.js / App.ts or wherever your routes to the pages are located.
19
+ If your project is TypeScript-based, you need to use the line from below as well. Import the component where you want to write your schemas. (See below for details about schemas)
18
20
 
19
21
  - `import { PageSchema } from 'admin_lsac'`
20
22
 
21
- If your project is TypeScript-based, you need to use the line above as well.
22
-
23
- Import the component from above where you want to write your schemas! (See below for details about schemas)
24
-
25
23
  ## How to add routes to the admin panel's pages?
26
24
 
27
25
  ```
@@ -68,7 +66,7 @@ export interface SimpleField {
68
66
  name: string;
69
67
  label: string;
70
68
  type: 'string' | 'number' | 'file' | 'date' | 'datetime-local' | 'time';
71
- validationSchema: any;
69
+ validationSchema?: any;
72
70
  }
73
71
 
74
72
  export interface Option {
@@ -104,13 +102,13 @@ export type FormField = SimpleField | DropdownField | ObjectField | ArrayField;
104
102
  export interface Action {
105
103
  name: string;
106
104
  fields?: FormField[];
107
- onSubmit: (obj1: any, obj2?: any, obj3?: any) => Promise<any>;
105
+ onSubmit: (obj1?: any, obj2?: any, obj3?: any) => Promise<any>;
108
106
  }
109
107
 
110
108
  export interface RowAction {
111
109
  name: string;
112
110
  fields?: FormField[];
113
- initialise?: (obj: any) => Promise<any>;
111
+ initialise?: (rowId: any) => Promise<any>;
114
112
  onSubmit: (obj1: any, obj2?: any, obj3?: any) => Promise<any>;
115
113
  }
116
114
 
@@ -135,7 +133,7 @@ What do the fields of `PageSchema` refer to?
135
133
  - getRequest = this is a field containg the function that will retrieve all the entries from one table in the database
136
134
  - tableFields = these are the 'definitions' of the displayed table's columns
137
135
 
138
- The operations for the rowActions and actions will generally have a form to complete before executing it. There are 4 types of inputs for the form part:
136
+ The operations for the rowActions and actions will generally (not mandatory) have a form to complete before executing it. There are 4 types of inputs for the form part:
139
137
 
140
138
  - simpleField = simple data like numbers, strings, files and dates
141
139
  - dropdownField = enable the user to choose an option from a predefined list
@@ -145,129 +143,365 @@ The operations for the rowActions and actions will generally have a form to comp
145
143
  #### Schema example
146
144
 
147
145
  ```
148
- import axios from "axios";
149
- import React from "react";
150
- const domain = "http://localhost:3001"; // dev mode
146
+ import React from 'react';
147
+ import axios from 'axios';
148
+ import * as Yup from 'yup';
149
+ import { PageSchema } from '../schema';
150
+
151
+ const domain = 'http://localhost:3001';
152
+
153
+ // Please try to modularize the code for a schema though
151
154
 
152
- const companies: PageSchema = {
153
- name: "Companies",
155
+ export const coursesPage: PageSchema = {
156
+ name: 'Courses',
154
157
 
155
- getRequest: async () => { // retrieve all entries from the database
156
- const res = await axios.get(domain + "/something");
157
- return res.data;
158
+ // 1. Data retrieval: fetch all courses from backend
159
+ getRequest: async () => {
160
+ const res = await axios.get(`${domain}/courses`);
161
+ return res.data; // returns an array of course objects
158
162
  },
159
163
 
160
- tableFields: [ // the columns displayed on the page
164
+ // 2. Table fields: define columns for the courses table
165
+ tableFields: [
161
166
  {
162
- name: "Company ID", // the ID must be displayed (and as first column),
163
- // if you would like to use rowActions ⚠️
164
- access: (obj: any) => obj.id, // obj.field_name
167
+ name: 'Course ID',
168
+ // The ID should be first column to allow rowActions to reference it (if rowActions needed)
169
+ access: (obj: any) => obj.id,
165
170
  },
166
171
  {
167
- name: "Name",
172
+ name: 'Name',
168
173
  access: (obj: any) => obj.name,
169
174
  },
170
175
  {
171
- name: "Logo",
172
- access: (obj: any) => // you can have even more complex functions
173
- obj.logoUrl
174
- ? React.createElement("img", {
175
- src: obj.logoUrl,
176
- alt: "ERROR",
177
- style: { width: "100px" },
178
- })
179
- : "No Image Available",
176
+ name: 'Level',
177
+ access: (obj: any) => obj.level,
178
+ },
179
+ {
180
+ name: 'Credits',
181
+ access: (obj: any) => obj.credits,
182
+ },
183
+ {
184
+ name: 'Start Date',
185
+ access: (obj: any) => (obj.startDate ? new Date(obj.startDate).toLocaleDateString() : 'N/A'),
186
+ },
187
+ {
188
+ name: 'Instructor',
189
+ // Display instructor name (assuming obj.instructor has { name, email })
190
+ access: (obj: any) => (obj.instructor ? obj.instructor.name : 'N/A'),
191
+ },
192
+ {
193
+ name: 'Course Image',
194
+ // Use React.createElement to render an image
195
+ access: (obj: any) =>
196
+ obj.coverImage
197
+ ? React.createElement('img', {
198
+ src: `${domain}${obj.coverImage}`,
199
+ alt: 'Cover Image',
200
+ style: { width: '100px' },
201
+ })
202
+ : 'No Image',
180
203
  },
181
204
  ],
182
205
 
206
+ // 3. Global actions (top-level page actions)
183
207
  actions: [
184
208
  {
185
- name: "Create New Quiz",
209
+ name: 'Ping Server',
210
+ // No form, no arguments: simple request to '/ping'
211
+ onSubmit: async () => {
212
+ const res = await axios.get(`${domain}/courses/ping`);
213
+ return res.data;
214
+ },
215
+ },
216
+
217
+ {
218
+ name: 'Create Course',
219
+ // Form with one argument (formValues) on submit
186
220
  fields: [
187
221
  {
188
- name: "name",
189
- label: "Quiz Name",
190
- type: "string", // for every FormField, type must be specified
191
- validationSchema: Yup.string().required(), // you can also require 'boolean' values with Yup
222
+ // Course Name
223
+ name: 'name',
224
+ label: 'Course Name',
225
+ type: 'string',
226
+ validationSchema: Yup.string().required('Name is required'),
227
+ },
228
+ {
229
+ // Level (Undergraduate/Graduate dropdown)
230
+ name: 'level',
231
+ label: 'Level',
232
+ type: 'dropdown',
233
+ validationSchema: Yup.string().required('Level is required'),
234
+ getOptions: async () => [
235
+ { value: 'Undergraduate', display: 'Undergraduate' },
236
+ { value: 'Graduate', display: 'Graduate' },
237
+ ],
238
+ },
239
+ {
240
+ // Credits
241
+ name: 'credits',
242
+ label: 'Credits',
243
+ type: 'number',
244
+ validationSchema: Yup.number().required('Credits are required'),
245
+ },
246
+ {
247
+ // Start Date
248
+ name: 'startDate',
249
+ label: 'Start Date',
250
+ type: 'date',
251
+ validationSchema: Yup.date().required('Start date is required'),
252
+ },
253
+ {
254
+ // Instructor (object with name and email)
255
+ name: 'instructor',
256
+ label: 'Instructor',
257
+ type: 'object',
258
+ fields: [
259
+ {
260
+ name: 'name',
261
+ label: 'Instructor Name',
262
+ type: 'string',
263
+ validationSchema: Yup.string().required('Instructor name is required'),
264
+ },
265
+ {
266
+ name: 'email',
267
+ label: 'Instructor Email',
268
+ type: 'string',
269
+ validationSchema: Yup.string()
270
+ .email('Invalid email')
271
+ .required('Instructor email is required'),
272
+ },
273
+ ],
192
274
  },
193
275
  {
194
- name: "questions",
195
- label: "Questions",
196
- type: "array",
276
+ // Schedule (array of sessions with date and topic)
277
+ name: 'schedule',
278
+ label: 'Schedule',
279
+ type: 'array',
197
280
  element: {
198
- name: "",
199
- label: "",
200
- type: "object",
281
+ name: '', // no specific name for array elements
282
+ label: '', // (fields below define each session)
283
+ type: 'object',
201
284
  fields: [
202
285
  {
203
- name: "text",
204
- label: "Question Text",
205
- type: "string",
206
- validationSchema: Yup.string().required(),
286
+ name: 'date',
287
+ label: 'Date',
288
+ type: 'date',
289
+ validationSchema: Yup.date().required('Session date is required'),
207
290
  },
208
291
  {
209
- name: "type",
210
- label: "Question Type",
211
- type: "dropdown",
212
- // validationSchema is missing here (optional), so it will not be a required field
213
- getOptions: async () => [
214
- {
215
- value: "OPEN",
216
- display: "Open",
217
- },
218
- {
219
- value: "SINGLE",
220
- display: "Single",
221
- },
222
- {
223
- value: "MULTIPLE",
224
- display: "Multiple",
225
- },
226
- ],
292
+ name: 'topic',
293
+ label: 'Topic',
294
+ type: 'string',
295
+ validationSchema: Yup.string().required('Session topic is required'),
227
296
  },
228
297
  ],
229
298
  },
230
299
  },
300
+ {
301
+ // Cover Image (file upload)
302
+ name: 'coverImage',
303
+ label: 'Cover Image',
304
+ type: 'file',
305
+ validationSchema: Yup.mixed().required('Cover image is required'),
306
+ },
231
307
  ],
232
- onSubmit: async (formValues: any) => { // onSubmit function can also be complex
233
- // for actions, onSubmit will always have one param (formValues - you can change the name though)
234
- // you can also process formVaues as you wish
235
- const result = await axios.post(domain + "/something", formValues);
236
- return result.data;
237
- }
308
+ onSubmit: async (formValues: any) => {
309
+ // Create a new course
310
+ const data = new FormData();
311
+ Object.entries(formValues).forEach(([k, v]) => {
312
+ if (k === 'coverImage') {
313
+ if (v instanceof File) data.append('coverImage', v);
314
+ } else if (k === 'schedule') {
315
+ data.append('schedule', JSON.stringify(v));
316
+ } else if (k === 'instructor') {
317
+ const instructor = v as { name: string; email: string };
318
+ data.append('instructor[name]', instructor.name);
319
+ data.append('instructor[email]', instructor.email);
320
+ } else {
321
+ data.append(k, v as any);
322
+ }
323
+ });
324
+
325
+ await axios.post(`${domain}/courses`, data, {
326
+ headers: { 'Content-Type': 'multipart/form-data' },
327
+ });
328
+ },
238
329
  },
239
330
  ],
240
331
 
332
+ // 4. Row-specific actions (actions for each course row)
241
333
  rowActions: [
242
334
  {
243
- name: "Modify Event - Variant 1", // without form
244
- onSubmit: async (eventId: number) => { // onSubmit has only param, the row's ID
245
- const res = await axios.delete(domain + "/something/" + eventId);
335
+ name: 'Delete Course',
336
+ // No form; onSubmit uses one argument (rowId) to delete that course
337
+ onSubmit: async (courseId: number) => {
338
+ const res = await axios.delete(`${domain}/courses/${courseId}`);
339
+ return res.data;
340
+ },
341
+ },
342
+
343
+ {
344
+ name: 'Archive Course',
345
+ // No form; onSubmit uses two arguments (rowId and rowObject) to archive
346
+ onSubmit: async (courseId: number, course: any) => {
347
+ console.log(course); // you can do whatever you want with the course information
348
+ const res = await axios.patch(`${domain}/courses/${courseId}/archive`);
349
+ return res.data;
350
+ },
351
+ },
352
+
353
+ {
354
+ name: 'Edit Basic Info',
355
+ // Form (name, level, credits) with two arguments onSubmit (formValues, rowId)
356
+ fields: [
357
+ {
358
+ name: 'name',
359
+ label: 'Course Name',
360
+ type: 'string',
361
+ validationSchema: Yup.string().required('Name is required'),
362
+ },
363
+ {
364
+ name: 'level',
365
+ label: 'Level',
366
+ type: 'dropdown',
367
+ validationSchema: Yup.string().required('Level is required'),
368
+ getOptions: async () => [
369
+ { value: 'Undergraduate', display: 'Undergraduate' },
370
+ { value: 'Graduate', display: 'Graduate' },
371
+ ],
372
+ },
373
+ {
374
+ name: 'credits',
375
+ label: 'Credits',
376
+ type: 'number',
377
+ validationSchema: Yup.number().required('Credits are required'),
378
+ },
379
+ ],
380
+ onSubmit: async (formValues: any, courseId: number) => {
381
+ // Update course basic info
382
+ const res = await axios.patch(`${domain}/courses/${courseId}`, formValues);
246
383
  return res.data;
247
384
  },
248
385
  },
386
+
249
387
  {
250
- name: "Modify Event - Variant 2", // with form
388
+ name: 'Edit Full Course',
389
+ // Form (all course fields) with three arguments onSubmit (formValues, rowId, rowObject)
251
390
  fields: [
252
391
  {
253
- name: "input",
254
- label: "Mock Field",
255
- type: "string",
256
- validationSchema: "",
392
+ name: 'name',
393
+ label: 'Course Name',
394
+ type: 'string',
395
+ validationSchema: Yup.string().required('Name is required'),
396
+ },
397
+ {
398
+ name: 'level',
399
+ label: 'Level',
400
+ type: 'dropdown',
401
+ validationSchema: Yup.string().required('Level is required'),
402
+ getOptions: async () => [
403
+ { value: 'Undergraduate', display: 'Undergraduate' },
404
+ { value: 'Graduate', display: 'Graduate' },
405
+ ],
406
+ },
407
+ {
408
+ name: 'credits',
409
+ label: 'Credits',
410
+ type: 'number',
411
+ validationSchema: Yup.number().required('Credits are required'),
412
+ },
413
+ {
414
+ name: 'startDate',
415
+ label: 'Start Date',
416
+ type: 'date',
417
+ validationSchema: Yup.date().required('Start date is required'),
418
+ },
419
+ {
420
+ name: 'instructor',
421
+ label: 'Instructor',
422
+ type: 'object',
423
+ fields: [
424
+ {
425
+ name: 'name',
426
+ label: 'Instructor Name',
427
+ type: 'string',
428
+ validationSchema: Yup.string().required('Instructor name is required'),
429
+ },
430
+ {
431
+ name: 'email',
432
+ label: 'Instructor Email',
433
+ type: 'string',
434
+ validationSchema: Yup.string()
435
+ .email('Invalid email')
436
+ .required('Instructor email is required'),
437
+ },
438
+ ],
439
+ },
440
+ {
441
+ name: 'schedule',
442
+ label: 'Schedule',
443
+ type: 'array',
444
+ element: {
445
+ name: '',
446
+ label: '',
447
+ type: 'object',
448
+ fields: [
449
+ {
450
+ name: 'date',
451
+ label: 'Date',
452
+ type: 'date',
453
+ validationSchema: Yup.date().required('Session date is required'),
454
+ },
455
+ {
456
+ name: 'topic',
457
+ label: 'Topic',
458
+ type: 'string',
459
+ validationSchema: Yup.string().required('Session topic is required'),
460
+ },
461
+ ],
462
+ },
463
+ },
464
+ {
465
+ name: 'coverImage',
466
+ label: 'Cover Image',
467
+ type: 'file',
468
+ validationSchema: Yup.mixed().required('Cover image is required'),
257
469
  },
258
470
  ],
259
- onSubmit: async (formValues: any, eventId: number) => { // onSubmit has two params
260
- // the first param - formValues, while the second is the row's ID
261
- const result = await axios.patch(domain + "/something" + eventId, formValues);
262
- return result.data;
471
+ // Use initialise to fetch current course data and pre-fill the form
472
+ initialise: async (courseId: any) => {
473
+ const res = await axios.get(`${domain}/courses/${courseId}`);
474
+ return res.data; // return course object to populate the form
475
+ },
476
+ onSubmit: async (formValues: any, courseId: number, course: any) => {
477
+ // Update full course details
478
+ // Please note that you can include "initialise" and not the "rowObject" (or vice versa)
479
+ console.log(course); // you can do whatever you want with the course information
480
+ const data = new FormData();
481
+
482
+ data.append('name', formValues.name);
483
+ data.append('level', formValues.level);
484
+ data.append('credits', formValues.credits);
485
+ data.append('startDate', formValues.startDate);
486
+
487
+ data.append('instructor[name]', formValues.instructor.name);
488
+ data.append('instructor[email]', formValues.instructor.email);
489
+
490
+ data.append('schedule', JSON.stringify(formValues.schedule));
491
+
492
+ if (formValues.coverImage) {
493
+ data.append('coverImage', formValues.coverImage);
494
+ }
495
+
496
+ await axios.patch(`${domain}/courses/${courseId}`, data, {
497
+ headers: { 'Content-Type': 'multipart/form-data' },
498
+ });
263
499
  },
264
500
  },
265
501
  ],
266
- }
502
+ };
267
503
  ```
268
504
 
269
- Please note that the schema above is not related to any entity, and the fields of PageSchema do not necessarily have any relation between them. These are just random examples to teach you how to create your own schema.
270
-
271
505
  #### An array of schemas must be defined as below:
272
506
 
273
507
  ```
@@ -8,8 +8,8 @@ import { MaterialReactTable } from 'material-react-table';
8
8
  import { mkConfig, generateCsv, download } from 'export-to-csv';
9
9
  import Swal from 'sweetalert2';
10
10
  import MenuItem from '@mui/material/MenuItem';
11
- import { io } from 'socket.io-client';
12
- import { newSocket } from '../utils/websockets';
11
+ // import { io } from 'socket.io-client';
12
+ // import { newSocket } from '../utils/websockets';
13
13
  export function pageToUrl(page) {
14
14
  return `${page.name.toLowerCase().replace(' ', '_')}`;
15
15
  }
@@ -59,24 +59,27 @@ export function AdminPage({ pages, selectedPage, basePath, apiURL = '', }) {
59
59
  const [rowId, setRowId] = useState();
60
60
  const [rowObject, setRowObject] = useState([]);
61
61
  const [refetch, setRefetch] = useState(false);
62
- const [socket, setSocket] = useState(io);
63
- newSocket(setSocket, apiURL);
62
+ // const [socket, setSocket] = useState(io);
63
+ // newSocket(setSocket, apiURL);
64
+ console.log(apiURL);
64
65
  useEffect(() => {
65
66
  selectedPage.getRequest().then((resArray) => {
66
67
  setData(resArray);
67
68
  setRefetch(false);
68
69
  });
69
70
  }, [refetch]); // Do not remove 'refetch' from here !!!
70
- useEffect(() => {
71
- socket.on('updateData', (message) => {
72
- console.log('Message emitted from socket: ' + message);
73
- setRefetch(true);
74
- const rowActionsMenu = document.querySelector('.MuiPaper-root.MuiPopover-paper.MuiMenu-paper');
75
- if (rowActionsMenu) {
76
- rowActionsMenu.remove();
77
- }
78
- });
79
- }, [socket, selectedPage]);
71
+ // useEffect(() => {
72
+ // socket.on('updateData', (message) => {
73
+ // console.log('Message emitted from socket: ' + message);
74
+ // setRefetch(true);
75
+ // const rowActionsMenu = document.querySelector(
76
+ // '.MuiPaper-root.MuiPopover-paper.MuiMenu-paper',
77
+ // );
78
+ // if (rowActionsMenu) {
79
+ // rowActionsMenu.remove();
80
+ // }
81
+ // });
82
+ // }, [socket, selectedPage]);
80
83
  useEffect(() => {
81
84
  setDisplayData(data.map((row) => selectedPage.tableFields.map((field) => field.access(row))));
82
85
  }, [data]);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "admin_lsac",
3
3
  "private": false,
4
- "version": "2.0.24",
4
+ "version": "2.0.26",
5
5
  "description": "Generic Admin Panel created for storing events' data",
6
6
  "scripts": {
7
7
  "dev": "vite",