admin_lsac 2.0.29 → 2.0.31

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
@@ -24,6 +24,8 @@ If your project is TypeScript-based, you need to use the line from below as well
24
24
 
25
25
  ## How to add routes to the admin panel's pages?
26
26
 
27
+ You will need to create an array of schemas (if more than one schema) for your own admin panel.
28
+
27
29
  ```
28
30
  {your_array.map((page, index) => (
29
31
  <Route
@@ -54,8 +56,6 @@ If your project is TypeScript-based, you need to use the line from below as well
54
56
  ) : null}
55
57
  ```
56
58
 
57
- You will need to create an array of schemas for your own admin panel.
58
-
59
59
  ## What is a schema?
60
60
 
61
61
  ```
@@ -142,397 +142,6 @@ The operations for the rowActions and actions will generally (not mandatory) hav
142
142
  - arrayField = arrays of data
143
143
  - objectField = an object containing more fields/inputs, or even other objects (some cool recursion)
144
144
 
145
- #### Schema example
146
-
147
- ```
148
- import React from 'react';
149
- import axios from 'axios';
150
- import * as Yup from 'yup';
151
- import { PageSchema } from '../schema';
152
-
153
- const domain = 'http://localhost:3001';
154
-
155
- // Please try to modularize the code for your schemas though
156
-
157
- export const coursesPage: PageSchema = {
158
- name: 'Courses',
159
-
160
- // 1. Data retrieval: fetch all courses from backend
161
- getRequest: async () => {
162
- const res = await axios.get(`${domain}/courses`);
163
- return res.data; // returns an array of course objects
164
- },
165
-
166
- // 2. Table fields: define columns for the courses table
167
- tableFields: [
168
- {
169
- name: 'Course ID',
170
- // The ID should be first column to allow rowActions to reference it (if rowActions needed)
171
- access: (obj: any) => obj.id,
172
- },
173
- {
174
- name: 'Name',
175
- access: (obj: any) => obj.name,
176
- },
177
- {
178
- name: 'Level',
179
- access: (obj: any) => obj.level,
180
- },
181
- {
182
- name: 'Credits',
183
- access: (obj: any) => obj.credits,
184
- },
185
- {
186
- name: 'Start Date',
187
- access: (obj: any) => (obj.startDate ? new Date(obj.startDate).toLocaleDateString() : 'N/A'),
188
- },
189
- {
190
- name: 'Instructor',
191
- // Display instructor name (assuming obj.instructor has { name, email })
192
- access: (obj: any) => (obj.instructor ? obj.instructor.name : 'N/A'),
193
- },
194
- {
195
- name: 'Course Image',
196
- // Use React.createElement to render an image
197
- access: (obj: any) =>
198
- obj.coverImage
199
- ? React.createElement('img', {
200
- src: `${domain}${obj.coverImage}`,
201
- alt: 'Cover Image',
202
- style: { width: '100px' },
203
- })
204
- : 'No Image',
205
- },
206
- ],
207
-
208
- // 3. Global actions (top-level page actions)
209
- actions: [
210
- {
211
- name: 'Ping Server',
212
- // No form, no arguments: simple request to '/ping'
213
- onSubmit: async () => {
214
- const res = await axios.get(`${domain}/courses/ping`);
215
- return res.data;
216
- },
217
- },
218
-
219
- {
220
- name: 'Create Course',
221
- // Form with one argument (formValues) on submit
222
- fields: [
223
- {
224
- // Course Name
225
- name: 'name',
226
- label: 'Course Name',
227
- type: 'string',
228
- validationSchema: Yup.string().required('Name is required'),
229
- },
230
- {
231
- // Level (Undergraduate/Graduate dropdown)
232
- name: 'level',
233
- label: 'Level',
234
- type: 'dropdown',
235
- validationSchema: Yup.string().required('Level is required'),
236
- // 'options' would have the same effect as 'getOptions', but not written as a function
237
- getOptions: async () => [
238
- { value: 'Undergraduate', display: 'Undergraduate' },
239
- { value: 'Graduate', display: 'Graduate' },
240
- ],
241
- },
242
- {
243
- // Credits
244
- name: 'credits',
245
- label: 'Credits',
246
- type: 'number',
247
- validationSchema: Yup.number().required('Credits are required'),
248
- },
249
- {
250
- // Start Date
251
- name: 'startDate',
252
- label: 'Start Date',
253
- type: 'date',
254
- validationSchema: Yup.date().required('Start date is required'),
255
- },
256
- {
257
- // Instructor (object with name and email)
258
- name: 'instructor',
259
- label: 'Instructor',
260
- type: 'object',
261
- fields: [
262
- {
263
- name: 'name',
264
- label: 'Instructor Name',
265
- type: 'string',
266
- validationSchema: Yup.string().required('Instructor name is required'),
267
- },
268
- {
269
- name: 'email',
270
- label: 'Instructor Email',
271
- type: 'string',
272
- validationSchema: Yup.string()
273
- .email('Invalid email')
274
- .required('Instructor email is required'),
275
- },
276
- ],
277
- },
278
- {
279
- // Schedule (array of sessions with date and topic)
280
- name: 'schedule',
281
- label: 'Schedule',
282
- type: 'array',
283
- element: {
284
- name: '', // no specific name for array elements
285
- label: '', // (fields below define each session)
286
- type: 'object',
287
- fields: [
288
- {
289
- name: 'date',
290
- label: 'Date',
291
- type: 'date',
292
- validationSchema: Yup.date().required('Session date is required'),
293
- },
294
- {
295
- name: 'topic',
296
- label: 'Topic',
297
- type: 'string',
298
- validationSchema: Yup.string().required('Session topic is required'),
299
- },
300
- ],
301
- },
302
- },
303
- {
304
- // Cover Image (file upload)
305
- name: 'coverImage',
306
- label: 'Cover Image',
307
- type: 'file',
308
- validationSchema: Yup.mixed().required('Cover image is required'),
309
- },
310
- ],
311
- onSubmit: async (formValues: any) => {
312
- // Create a new course
313
- const data = new FormData();
314
- Object.entries(formValues).forEach(([k, v]) => {
315
- if (k === 'coverImage') {
316
- if (v instanceof File) data.append('coverImage', v);
317
- } else if (k === 'schedule') {
318
- data.append('schedule', JSON.stringify(v));
319
- } else if (k === 'instructor') {
320
- const instructor = v as { name: string; email: string };
321
- data.append('instructor[name]', instructor.name);
322
- data.append('instructor[email]', instructor.email);
323
- } else {
324
- data.append(k, v as any);
325
- }
326
- });
327
-
328
- await axios.post(`${domain}/courses`, data, {
329
- headers: { 'Content-Type': 'multipart/form-data' },
330
- });
331
- },
332
- },
333
- ],
334
-
335
- // 4. Row-specific actions (actions for each course row)
336
- rowActions: [
337
- {
338
- name: 'Delete Course',
339
- // No form; onSubmit uses one argument (rowId) to delete that course
340
- onSubmit: async (courseId: number) => {
341
- const res = await axios.delete(`${domain}/courses/${courseId}`);
342
- return res.data;
343
- },
344
- },
345
-
346
- {
347
- name: 'Archive Course',
348
- // No form; onSubmit uses two arguments (rowId and rowObject) to archive
349
- onSubmit: async (courseId: number, course: any) => {
350
- console.log(course); // you can do whatever you want with the course information
351
- const res = await axios.patch(`${domain}/courses/${courseId}/archive`);
352
- return res.data;
353
- },
354
- },
355
-
356
- {
357
- name: 'Edit Basic Info',
358
- // Form (name, level, credits) with two arguments onSubmit (formValues, rowId)
359
- fields: [
360
- {
361
- name: 'name',
362
- label: 'Course Name',
363
- type: 'string',
364
- validationSchema: Yup.string().required('Name is required'),
365
- },
366
- {
367
- name: 'level',
368
- label: 'Level',
369
- type: 'dropdown',
370
- validationSchema: Yup.string().required('Level is required'),
371
- getOptions: async () => [
372
- { value: 'Undergraduate', display: 'Undergraduate' },
373
- { value: 'Graduate', display: 'Graduate' },
374
- ],
375
- },
376
- {
377
- name: 'credits',
378
- label: 'Credits',
379
- type: 'number',
380
- validationSchema: Yup.number().required('Credits are required'),
381
- },
382
- ],
383
- onSubmit: async (formValues: any, courseId: number) => {
384
- // Update course basic info
385
- const res = await axios.patch(`${domain}/courses/${courseId}`, formValues);
386
- return res.data;
387
- },
388
- },
389
-
390
- {
391
- name: 'Edit Full Course',
392
- // Form (all course fields) with three arguments onSubmit (formValues, rowId, rowObject)
393
- fields: [
394
- {
395
- name: 'name',
396
- label: 'Course Name',
397
- type: 'string',
398
- validationSchema: Yup.string().required('Name is required'),
399
- },
400
- {
401
- name: 'level',
402
- label: 'Level',
403
- type: 'dropdown',
404
- validationSchema: Yup.string().required('Level is required'),
405
- getOptions: async () => [
406
- { value: 'Undergraduate', display: 'Undergraduate' },
407
- { value: 'Graduate', display: 'Graduate' },
408
- ],
409
- },
410
- {
411
- name: 'credits',
412
- label: 'Credits',
413
- type: 'number',
414
- validationSchema: Yup.number().required('Credits are required'),
415
- },
416
- {
417
- name: 'startDate',
418
- label: 'Start Date',
419
- type: 'date',
420
- validationSchema: Yup.date().required('Start date is required'),
421
- },
422
- {
423
- name: 'instructor',
424
- label: 'Instructor',
425
- type: 'object',
426
- fields: [
427
- {
428
- name: 'name',
429
- label: 'Instructor Name',
430
- type: 'string',
431
- validationSchema: Yup.string().required('Instructor name is required'),
432
- },
433
- {
434
- name: 'email',
435
- label: 'Instructor Email',
436
- type: 'string',
437
- validationSchema: Yup.string()
438
- .email('Invalid email')
439
- .required('Instructor email is required'),
440
- },
441
- ],
442
- },
443
- {
444
- name: 'schedule',
445
- label: 'Schedule',
446
- type: 'array',
447
- element: {
448
- name: '',
449
- label: '',
450
- type: 'object',
451
- fields: [
452
- {
453
- name: 'date',
454
- label: 'Date',
455
- type: 'date',
456
- validationSchema: Yup.date().required('Session date is required'),
457
- },
458
- {
459
- name: 'topic',
460
- label: 'Topic',
461
- type: 'string',
462
- validationSchema: Yup.string().required('Session topic is required'),
463
- },
464
- ],
465
- },
466
- },
467
- {
468
- name: 'coverImage',
469
- label: 'Cover Image',
470
- type: 'file',
471
- validationSchema: Yup.mixed().required('Cover image is required'),
472
- },
473
- ],
474
- // Use initialise to fetch current course data and pre-fill the form
475
- initialise: async (courseId: any) => {
476
- const res = await axios.get(`${domain}/courses/${courseId}`);
477
- return res.data; // return course object to populate the form
478
- },
479
- onSubmit: async (formValues: any, courseId: number, course: any) => {
480
- // Update full course details
481
- // Please note that you can include "initialise" and not the "rowObject" (or vice versa)
482
- console.log(course); // you can do whatever you want with the course information
483
- const data = new FormData();
484
-
485
- data.append('name', formValues.name);
486
- data.append('level', formValues.level);
487
- data.append('credits', formValues.credits);
488
- data.append('startDate', formValues.startDate);
489
-
490
- data.append('instructor[name]', formValues.instructor.name);
491
- data.append('instructor[email]', formValues.instructor.email);
492
-
493
- data.append('schedule', JSON.stringify(formValues.schedule));
494
-
495
- if (formValues.coverImage) {
496
- data.append('coverImage', formValues.coverImage);
497
- }
498
-
499
- await axios.patch(`${domain}/courses/${courseId}`, data, {
500
- headers: { 'Content-Type': 'multipart/form-data' },
501
- });
502
- },
503
- },
504
- ],
505
- };
506
- ```
507
-
508
- #### An array of schemas must be defined as below:
509
-
510
- ```
511
- const your_array = [
512
- {
513
- // schema 1
514
- },
515
- {
516
- // schema 2
517
- },
518
- ...
519
- ];
520
- ```
521
-
522
- - In TypeScript, it would slightly change:
523
-
524
- ```
525
- const your_array: PageSchema[] = [
526
- {
527
- // schema 1
528
- },
529
- {
530
- // schema 2
531
- },
532
- ...
533
- ];
534
- ```
535
-
536
145
  ## Websockets Support
537
146
 
538
147
  For real-time changes, this NPM module uses socket.io-client library to implement WebSockets. So, on the server-side, you will have to use socket.io library, and provide an `apiURL` as shown above.
@@ -547,4 +156,4 @@ For every entity you describe through a page schema, getRequest and onSubmit fun
547
156
 
548
157
  ## Learn More
549
158
 
550
- For more details, especially about page schemas, contact the owner. :)
159
+ For more details, especially about page schemas, contact the owner (LSAC Bucuresti). :)
@@ -99,16 +99,14 @@ export function AdminPage({ pages, selectedPage, basePath, apiURL = '', }) {
99
99
  overflowY: 'visible',
100
100
  overflowX: 'auto',
101
101
  },
102
- }, enableColumnResizing: true, enableColumnPinning: true, enableGlobalFilterModes: true, enablePagination: false, paginationDisplayMode: "pages", enableRowVirtualization: true, rowVirtualizerOptions: { overscan: 20 }, renderTopToolbarCustomActions: () => (_jsx(ChakraProvider, { children: _jsx(Stack, { margin: 0, padding: 2, direction: "row", wrap: "wrap", children: selectedPage.actions.map((action, index) => (_jsx(Button, { colorScheme: "blue", onClick: () => {
102
+ }, enableColumnResizing: true, enableColumnPinning: true, enableGlobalFilterModes: true, enablePagination: false, paginationDisplayMode: "pages", enableRowVirtualization: false, renderTopToolbarCustomActions: () => (_jsx(ChakraProvider, { children: _jsx(Stack, { margin: 0, padding: 2, direction: "row", wrap: "wrap", children: selectedPage.actions.map((action, index) => (_jsx(Button, { colorScheme: "blue", onClick: () => {
103
103
  setSelectedAction(action);
104
104
  onOpen();
105
105
  }, children: action.name }, index))) }) })), enableRowActions: true, renderRowActionMenuItems: (obj) => {
106
106
  return selectedPage.rowActions.map((rowAction, index) => (_jsx(MenuItem, { onClick: () => {
107
107
  setSelectedAction(rowAction);
108
108
  setRowId(obj.row.original[0]);
109
- if (obj.staticRowIndex !== undefined) {
110
- setRowObject(data[obj.staticRowIndex]);
111
- }
109
+ setRowObject(data[obj.row.index]);
112
110
  onOpen();
113
111
  const rowActionsMenu = document.querySelector('.MuiPaper-root.MuiPopover-paper.MuiMenu-paper');
114
112
  if (rowActionsMenu) {
@@ -1,5 +1,5 @@
1
- import React from 'react';
2
1
  import axios from 'axios';
2
+ import React from 'react';
3
3
  import * as Yup from 'yup';
4
4
  const domain = 'http://localhost:3001';
5
5
  // Please try to modularize the code for your schemas though
@@ -35,7 +35,6 @@ export const coursesPage = {
35
35
  },
36
36
  {
37
37
  name: 'Instructor',
38
- // Display instructor name (assuming obj.instructor has { name, email })
39
38
  access: (obj) => (obj.instructor ? obj.instructor.name : 'N/A'),
40
39
  },
41
40
  {
@@ -125,8 +124,8 @@ export const coursesPage = {
125
124
  label: 'Schedule',
126
125
  type: 'array',
127
126
  element: {
128
- name: '', // no specific name for array elements
129
- label: '', // (fields below define each session)
127
+ name: '', // no specific name required for array elements, it will not be displayed
128
+ label: '', // no specific label required for array elements, it will not be displayed
130
129
  type: 'object',
131
130
  fields: [
132
131
  {
@@ -172,9 +171,25 @@ export const coursesPage = {
172
171
  data.append(k, v);
173
172
  }
174
173
  });
175
- await axios.post(`${domain}/courses`, data, {
174
+ const res = await axios.post(`${domain}/courses`, data, {
176
175
  headers: { 'Content-Type': 'multipart/form-data' },
177
176
  });
177
+ return res.data;
178
+ },
179
+ },
180
+ {
181
+ name: 'Download All Course Photos',
182
+ // Example for downloading different objects (photos, CVs etc.)
183
+ onSubmit: async () => {
184
+ const res = await axios.get(`${domain}/courses/photos/download`, { responseType: 'blob' });
185
+ const url = window.URL.createObjectURL(new Blob([res.data], { type: 'application/zip' }));
186
+ const link = document.createElement('a');
187
+ link.href = url;
188
+ link.download = 'courses-images.zip';
189
+ document.body.appendChild(link);
190
+ link.click();
191
+ link.remove();
192
+ window.URL.revokeObjectURL(url);
178
193
  },
179
194
  },
180
195
  ],
@@ -334,9 +349,10 @@ export const coursesPage = {
334
349
  if (formValues.coverImage) {
335
350
  data.append('coverImage', formValues.coverImage);
336
351
  }
337
- await axios.patch(`${domain}/courses/${courseId}`, data, {
352
+ const res = await axios.patch(`${domain}/courses/${courseId}`, data, {
338
353
  headers: { 'Content-Type': 'multipart/form-data' },
339
354
  });
355
+ return res.data;
340
356
  },
341
357
  },
342
358
  ],
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "admin_lsac",
3
3
  "private": false,
4
- "version": "2.0.29",
4
+ "version": "2.0.31",
5
5
  "description": "Generic Admin Panel created for storing events' data",
6
6
  "scripts": {
7
7
  "dev": "vite",