@topconsultnpm/sdkui-react 6.22.0-dev2.47 → 6.22.0-dev2.49

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.
@@ -24,11 +24,9 @@ import TMToppyMessage from '../../../helper/TMToppyMessage';
24
24
  import TMMetadataSorterForm from './TMMetadataSorterForm';
25
25
  import TMMetadataOutputForm from './TMMetadataOutputForm';
26
26
  import TMTooltip from '../../base/TMTooltip';
27
- const TMSearchQueryPanel = ({ isSearchViewOnScreen = true, fromDTD, showBackToResultButton, isExpertMode = SDKUI_Globals.userSettings.advancedSettings.expertMode === 1, SQD, inputMids, onSearchCompleted, onSqdSaved, onBack, onClosePanel, allowMaximize = true, onMaximizePanel, onBackToResult, passToArchiveCallback, maxDcmtsToBeReturned }) => {
27
+ const TMSearchQueryPanel = ({ isSearchViewOnScreen = true, fromDTD, showBackToResultButton, isExpertMode = SDKUI_Globals.userSettings.advancedSettings.expertMode === 1, SQD, inputMids, onSearchCompleted, onSqdSaved, onBack, onClosePanel, allowMaximize = true, onMaximizePanel, onBackToResult, passToArchiveCallback, maxDcmtsToBeReturned, onRefreshRequested, onDcmtTypeReloaded }) => {
28
28
  const [confirmQueryParams, ConfirmQueryParamsDialog] = useQueryParametersDialog();
29
29
  const [qd, setQd] = useState();
30
- const [shouldSearch, setShouldSearch] = useState(false);
31
- const [isQueryPanelActive, setIsQueryPanelActive] = useState(false);
32
30
  const [lastQdParams, setLastQdParams] = useState([]);
33
31
  const [dcmtTypesList, setDcmtTypesList] = useState([]);
34
32
  const [showSqdForm, setShowSqdForm] = useState(false);
@@ -45,12 +43,19 @@ const TMSearchQueryPanel = ({ isSearchViewOnScreen = true, fromDTD, showBackToRe
45
43
  const isMobile = deviceType === DeviceType.MOBILE;
46
44
  let initialMaxItems = deviceType === DeviceType.MOBILE ? 8 : 12;
47
45
  const tmPanelRef = useRef(null);
46
+ // Contenitore del form: l'Invio avvia la ricerca solo se il focus si trova al suo interno
47
+ const contentRef = useRef(null);
48
48
  // Passando nella vista risultati si prenota il focus, che verrà ripreso al rientro nella ricerca
49
49
  const shouldRefocusOnReturnRef = useRef(false);
50
50
  const appliedInputMidsRef = useRef(null);
51
51
  const pendingMidsRef = useRef(null);
52
- // Flag "accoda": impostato dall'azione secondaria del bottone Ricerca, consumato in executeSearch
53
- const appendNextSearchRef = useRef(false);
52
+ // Impedisce ricerche sovrapposte (doppio Invio, o Invio mentre la precedente e' ancora in corso)
53
+ const isSearchingRef = useRef(false);
54
+ // Snapshot letti da runSearch al momento dell'esecuzione: evitano di cercare con dati stantii
55
+ const qdRef = useRef(undefined);
56
+ const showAdvancedSearchRef = useRef(false);
57
+ const searchAsyncRef = useRef();
58
+ const reloadMetadataAsyncRef = useRef();
54
59
  // Tornando dalla vista risultati il pannello dei metadati di ricerca riprende il focus,
55
60
  // così è lui quello evidenziato: prendendo il focus gli altri pannelli ricevono l'onBlur e si disattivano
56
61
  useEffect(() => {
@@ -135,39 +140,6 @@ const TMSearchQueryPanel = ({ isSearchViewOnScreen = true, fromDTD, showBackToRe
135
140
  setQd({ ...qd, where: newWhere });
136
141
  setShowAllMdWhere(true);
137
142
  }, [qd, fromDTD, inputMids]);
138
- // Eseguire la ricerca quando shouldSearch è true e qd è definito
139
- useEffect(() => {
140
- if (shouldSearch && qd) {
141
- const executeSearch = async () => {
142
- // Legge e consuma il flag "accoda" impostato dall'azione secondaria del bottone Ricerca
143
- const append = appendNextSearchRef.current;
144
- appendNextSearchRef.current = false;
145
- await searchAsync(qd, showAdvancedSearch, append);
146
- setShouldSearch(false); // Resetta il trigger dopo la ricerca
147
- };
148
- executeSearch();
149
- }
150
- }, [shouldSearch, qd, showAdvancedSearch]);
151
- // Se il pannello è attivo e non ci sono modali aperti, rimuove il focus e avvia la ricerca
152
- useEffect(() => {
153
- const handleKeyDown = (e) => {
154
- if (isQueryPanelActive && !modalIsOpen) {
155
- if (e.key === 'Enter') {
156
- e.preventDefault();
157
- // Disattiva l'elemento attivo per forzare l'onBlur
158
- if (document.activeElement instanceof HTMLElement) {
159
- document.activeElement.blur();
160
- }
161
- // Attiva il flag di ricerca. L'useEffect sopra gestirà l'attesa di qd.
162
- setShouldSearch(true);
163
- }
164
- }
165
- };
166
- window.addEventListener('keydown', handleKeyDown);
167
- return () => {
168
- window.removeEventListener('keydown', handleKeyDown);
169
- };
170
- }, [isQueryPanelActive, modalIsOpen]);
171
143
  const setDataAsync = async (sqd) => {
172
144
  let newSqd = (sqd?.id == 1) ? sqd : await SavedQueryCacheService.GetAsync(sqd?.id);
173
145
  let newQd = SearchEngine.NormalizeQueryDescriptor(newSqd?.qd);
@@ -199,11 +171,92 @@ const TMSearchQueryPanel = ({ isSearchViewOnScreen = true, fromDTD, showBackToRe
199
171
  let results = [];
200
172
  if (searchResult?.result)
201
173
  results.push(searchResult?.result);
202
- // Before notifying onSearchComplete, let's set the panel to NOT active
203
- setIsQueryPanelActive(false);
204
174
  onSearchCompleted?.(results, searchResult?.qd, append);
205
175
  }
206
176
  };
177
+ // Tra l'apertura della ricerca e il click su Ricerca la lista dei metadati del tipo documento può essere
178
+ // cambiata (metadati aggiunti o rimossi lato amministrazione): prima di cercare si rileggono dal server.
179
+ // I valori digitati restano, mentre i filtri sui metadati non più disponibili vengono scartati.
180
+ // Restituisce il qd con cui eseguire la ricerca.
181
+ const reloadMetadataAsync = async (qdInput) => {
182
+ if (!qdInput?.from?.tid)
183
+ return qdInput;
184
+ let dcmtTypes;
185
+ try {
186
+ TMSpinner.show({ description: `${SDKUI_Localizator.Loading} ...`, backgroundColor: 'transparent' });
187
+ dcmtTypes = await getDcmtTypesByQdAsync(qdInput, true);
188
+ }
189
+ finally {
190
+ TMSpinner.hide();
191
+ }
192
+ // Rilettura non riuscita: si cerca con i metadati che si hanno, senza scartare nulla
193
+ if (dcmtTypes.length <= 0)
194
+ return qdInput;
195
+ setDcmtTypesList(dcmtTypes);
196
+ const reloadedFromDTD = dcmtTypes.find(o => o.id === qdInput.from?.tid);
197
+ if (reloadedFromDTD)
198
+ onDcmtTypeReloaded?.(reloadedFromDTD);
199
+ const { qd: newQd, removedCount } = dropUnavailableMetadataFromQd(qdInput, dcmtTypes);
200
+ if (removedCount <= 0)
201
+ return qdInput;
202
+ setQd(newQd);
203
+ return newQd;
204
+ };
205
+ // Allineati a ogni render: runSearch li legge quando esegue, non quando viene creata
206
+ qdRef.current = qd;
207
+ showAdvancedSearchRef.current = showAdvancedSearch;
208
+ searchAsyncRef.current = searchAsync;
209
+ reloadMetadataAsyncRef.current = reloadMetadataAsync;
210
+ // Unico punto di avvio della ricerca: bottone Ricerca, Ricerca e accoda e tasto Invio passano tutti da qui
211
+ const runSearch = useCallback((append) => {
212
+ if (isSearchingRef.current)
213
+ return;
214
+ isSearchingRef.current = true;
215
+ // Toglie il focus per far committare il valore del campo in editing
216
+ if (document.activeElement instanceof HTMLElement)
217
+ document.activeElement.blur();
218
+ // Alcuni editor committano il valore in modo asincrono: si attende un tick prima di leggere il qd definitivo
219
+ setTimeout(async () => {
220
+ try {
221
+ // I metadati si rileggono a ogni ricerca: si cerca sempre sulla lista aggiornata
222
+ const qdToSearch = await reloadMetadataAsyncRef.current?.(qdRef.current) ?? qdRef.current;
223
+ await searchAsyncRef.current?.(qdToSearch, showAdvancedSearchRef.current, append);
224
+ }
225
+ catch (ex) {
226
+ TMExceptionBoxManager.show({ exception: ex });
227
+ }
228
+ finally {
229
+ isSearchingRef.current = false;
230
+ }
231
+ }, 0);
232
+ }, []);
233
+ // I dialoghi (TMModal/chooser) escono da contentRef perche' DevExtreme Popup li rende in un portale su document.body:
234
+ // il controllo di contenimento li esclude gia'. Questi flag coprono il caso in cui il focus sia sul contenitore del pannello.
235
+ // NB: si usano solo flag locali affidabili, non modalIsOpen, che resterebbe a true se un chooser venisse smontato senza onClose.
236
+ const isAnyDialogOpen = showFiltersConfig || showOutputConfig || showMetadataSorterForm || showSqdForm || showDistinctValuesPanel;
237
+ // L'Invio avvia SEMPRE la ricerca quando il focus e' nel form di ricerca (o sul pannello che lo contiene).
238
+ // Il criterio e' il contenimento nel DOM, non uno stato di "pannello attivo" che puo' desincronizzarsi.
239
+ useEffect(() => {
240
+ const handleKeyDown = (e) => {
241
+ if (e.key !== 'Enter' || e.repeat)
242
+ return;
243
+ if (isAnyDialogOpen)
244
+ return;
245
+ const content = contentRef.current;
246
+ const target = e.target;
247
+ if (!content || !target)
248
+ return;
249
+ const isInsideForm = content.contains(target);
250
+ // Al rientro dai risultati il focus e' sul contenitore del pannello, che e' antenato del form
251
+ const isPanelContainer = target !== document.body && target !== document.documentElement && target.contains(content);
252
+ if (!isInsideForm && !isPanelContainer)
253
+ return;
254
+ e.preventDefault();
255
+ runSearch(false);
256
+ };
257
+ window.addEventListener('keydown', handleKeyDown);
258
+ return () => { window.removeEventListener('keydown', handleKeyDown); };
259
+ }, [isAnyDialogOpen, runSearch]);
207
260
  const updateIsModalOpen = useCallback((isOpen) => {
208
261
  setModalIsOpen(isOpen);
209
262
  }, []);
@@ -230,36 +283,15 @@ const TMSearchQueryPanel = ({ isSearchViewOnScreen = true, fromDTD, showBackToRe
230
283
  }
231
284
  }
232
285
  };
233
- const handleSearchButtonClick = useCallback(async () => {
234
- // Propagazione blur
235
- if (document.activeElement instanceof HTMLElement) {
236
- document.activeElement.blur();
237
- }
238
- // Ricerca normale: azzera l'eventuale flag accoda e avvia
239
- appendNextSearchRef.current = false;
240
- setShouldSearch(true);
241
- }, []);
242
- const handleSearchAndAppendClick = useCallback(() => {
243
- // Propagazione blur
244
- if (document.activeElement instanceof HTMLElement) {
245
- document.activeElement.blur();
246
- }
247
- // Ricerca e accoda: imposta il flag consumato da executeSearch
248
- appendNextSearchRef.current = true;
249
- setShouldSearch(true);
250
- }, []);
286
+ const handleSearchButtonClick = useCallback(() => { runSearch(false); }, [runSearch]);
287
+ const handleSearchAndAppendClick = useCallback(() => { runSearch(true); }, [runSearch]);
251
288
  const searchButtonSecondaryActions = useMemo(() => [{
252
289
  name: SDKUI_Localizator.SearchAndAppend,
253
290
  icon: _jsx(IconSearch, {}),
254
291
  onClick: handleSearchAndAppendClick
255
292
  }], [handleSearchAndAppendClick]);
256
293
  const handleQdChanged = useCallback((newQd) => {
257
- if (!deepCompare(qd, newQd)) {
258
- setQd(newQd);
259
- }
260
- }, []);
261
- const handlePanelActiveChanged = useCallback((isActive) => {
262
- setIsQueryPanelActive(isActive);
294
+ setQd(prev => deepCompare(prev, newQd) ? prev : newQd);
263
295
  }, []);
264
296
  const handleAdvancedMenuClick = useCallback((e) => {
265
297
  if (e.button === AdvancedMenuButtons.DistinctValues) {
@@ -357,10 +389,10 @@ const TMSearchQueryPanel = ({ isSearchViewOnScreen = true, fromDTD, showBackToRe
357
389
  return false;
358
390
  return qd?.orderBy && qd.orderBy.length > 0;
359
391
  }, [showAdvancedSearch, qd?.orderBy]);
360
- return (_jsxs(_Fragment, { children: [_jsxs(TMPanel, { ref: tmPanelRef, title: fromDTD?.nameLoc ?? SDKUI_Localizator.Search_Metadata, allowMaximize: allowMaximize, onMaximize: onMaximizePanel, onHeaderDoubleClick: onMaximizePanel, onBack: onBack, onActiveChanged: handlePanelActiveChanged, toolbar: _jsx(_Fragment, { children: (SQD && !showSqdForm) ?
361
- _jsx(ContextMenu, { items: contextMenuItems, trigger: "left", children: _jsx(TMButton, { btnStyle: 'icon', caption: 'Altro', icon: _jsx(IconMenuVertical, { color: 'white' }), showTooltip: false, onClick: () => setIsQueryPanelActive(true) }) })
392
+ return (_jsxs(_Fragment, { children: [_jsxs(TMPanel, { ref: tmPanelRef, title: fromDTD?.nameLoc ?? SDKUI_Localizator.Search_Metadata, allowMaximize: allowMaximize, onMaximize: onMaximizePanel, onHeaderDoubleClick: onMaximizePanel, onBack: onBack, onRefresh: (!SQD || showSqdForm) ? undefined : onRefreshRequested, toolbar: _jsx(_Fragment, { children: (SQD && !showSqdForm) ?
393
+ _jsx(ContextMenu, { items: contextMenuItems, trigger: "left", children: _jsx(TMButton, { btnStyle: 'icon', caption: 'Altro', icon: _jsx(IconMenuVertical, { color: 'white' }), showTooltip: false }) })
362
394
  : _jsx(_Fragment, {}) }), children: [_jsx(ConfirmQueryParamsDialog, {}), SQD
363
- ? _jsxs("div", { onContextMenu: (e) => e.preventDefault(), style: { height: '100%', width: '100%', position: 'relative', display: showSqdForm ? 'none' : 'flex', flexDirection: 'column', gap: 5 }, children: [showAdvancedSearch
395
+ ? _jsxs("div", { ref: contentRef, onContextMenu: (e) => e.preventDefault(), style: { height: '100%', width: '100%', position: 'relative', display: showSqdForm ? 'none' : 'flex', flexDirection: 'column', gap: 5 }, children: [showAdvancedSearch
364
396
  ? _jsx(TMQueryEditor, { formMode: FormModes.Update, showToolbar: false, inputData: qd, validateSelect: true, showApply: false, onQDChanged: handleQdChanged, updateIsModalOpen: updateIsModalOpen })
365
397
  : _jsx(TMSearchQueryEditor, { qd: qd, dcmtTypesList: dcmtTypesList, isExpertMode: isExpertMode, showAllMdWhere: showAllMdWhere, onQdChanged: handleQdChanged, onFocusedMetadataChanged: setFocusedTidMid, onAdvancedMenuClick: handleAdvancedMenuClick, updateIsModalOpen: updateIsModalOpen }), shouldShowOutputBadges && qd?.select && qd.select.length > 0 && (_jsxs(StyledSelectBadgesContainer, { children: [_jsxs(StyledSelectLabel, { children: [SDK_Localizator.QuerySelect, ":"] }), _jsx(StyledSelectBadgesWrapper, { children: (() => {
366
398
  const selectItems = qd.select ?? [];
@@ -418,6 +450,33 @@ const TMSearchQueryPanel = ({ isSearchViewOnScreen = true, fromDTD, showBackToRe
418
450
  } })] }));
419
451
  };
420
452
  export default TMSearchQueryPanel;
453
+ /** Scarta dal qd filtri, colonne e ordinamenti sui metadati non più esistenti o non più accessibili, in base
454
+ * alla lista dei metadati appena riletta dal server. I valori digitati su quelli ancora presenti si mantengono. */
455
+ const dropUnavailableMetadataFromQd = (qdInput, dcmtTypes) => {
456
+ const metadataOf = (tid) => dcmtTypes.find(o => o.id === tid)?.metadata;
457
+ // Un tipo documento non riletto non è verificabile: i suoi item si mantengono così come sono.
458
+ // Sui metadati di sistema si verifica solo l'esistenza: i loro permessi non li rendono inutilizzabili.
459
+ const isStillAvailable = (tid, mid, isUsable) => {
460
+ const metadata = metadataOf(tid);
461
+ if (!metadata || metadata.length <= 0 || mid == undefined)
462
+ return true;
463
+ const md = metadata.find(o => o.id === mid);
464
+ if (!md)
465
+ return false;
466
+ return md.isSystem == 1 || isUsable(md);
467
+ };
468
+ const canSearch = (md) => md.perm?.canSearch != AccessLevels.No;
469
+ const canOutput = (md) => !(md.perm?.canView == AccessLevels.No && md.perm?.canUpdate == AccessLevels.No);
470
+ const where = qdInput.where?.filter(o => isStillAvailable(o.tid, o.mid, canSearch)) ?? [];
471
+ const select = qdInput.select?.filter(o => isStillAvailable(o.tid, o.mid, canOutput)) ?? [];
472
+ const orderBy = qdInput.orderBy?.filter(o => isStillAvailable(o.tid, o.mid, canOutput)) ?? [];
473
+ const removedCount = ((qdInput.where?.length ?? 0) - where.length)
474
+ + ((qdInput.select?.length ?? 0) - select.length)
475
+ + ((qdInput.orderBy?.length ?? 0) - orderBy.length);
476
+ let qd = new QueryDescriptor();
477
+ qd.init({ ...qdInput, where: where, select: select, orderBy: orderBy });
478
+ return { qd: qd, removedCount: removedCount };
479
+ };
421
480
  const searchByQdAsync = async (qdInput, searchParams) => {
422
481
  let result;
423
482
  let qdSearch;
@@ -475,205 +534,205 @@ export const refreshLastSearch = async (qd) => {
475
534
  // =============================================================================
476
535
  // STYLED COMPONENTS per i badge degli ordinamenti
477
536
  // =============================================================================
478
- const StyledOrderByBadgesContainer = styled.div `
479
- display: flex;
480
- flex-wrap: wrap;
481
- align-items: center;
482
- gap: 8px;
483
- padding: 6px 10px;
484
- margin: 1px 0;
485
- background: linear-gradient(135deg, rgba(74, 150, 210, 0.08) 0%, rgba(37, 89, 165, 0.08) 100%);
486
- border-radius: 8px;
487
- border: 1px solid rgba(74, 150, 210, 0.2);
537
+ const StyledOrderByBadgesContainer = styled.div `
538
+ display: flex;
539
+ flex-wrap: wrap;
540
+ align-items: center;
541
+ gap: 8px;
542
+ padding: 6px 10px;
543
+ margin: 1px 0;
544
+ background: linear-gradient(135deg, rgba(74, 150, 210, 0.08) 0%, rgba(37, 89, 165, 0.08) 100%);
545
+ border-radius: 8px;
546
+ border: 1px solid rgba(74, 150, 210, 0.2);
488
547
  `;
489
- const StyledOrderByLabel = styled.span `
490
- font-size: 0.82rem;
491
- font-weight: 600;
492
- color: #4A96D2;
493
- white-space: nowrap;
548
+ const StyledOrderByLabel = styled.span `
549
+ font-size: 0.82rem;
550
+ font-weight: 600;
551
+ color: #4A96D2;
552
+ white-space: nowrap;
494
553
  `;
495
- const StyledOrderByBadgesWrapper = styled.div `
496
- display: flex;
497
- flex-wrap: wrap;
498
- gap: 6px;
499
- align-items: center;
500
- flex: 1;
554
+ const StyledOrderByBadgesWrapper = styled.div `
555
+ display: flex;
556
+ flex-wrap: wrap;
557
+ gap: 6px;
558
+ align-items: center;
559
+ flex: 1;
501
560
  `;
502
- const StyledOrderByBadge = styled.div `
503
- display: inline-flex;
504
- align-items: center;
505
- gap: 4px;
506
- padding: 3px 8px;
507
- background: white;
508
- border: 1px solid #d1d9e6;
509
- border-radius: 16px;
510
- font-size: 0.82rem;
511
- color: #333;
512
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
513
-
514
- .badge-name {
515
- max-width: 120px;
516
- overflow: hidden;
517
- text-overflow: ellipsis;
518
- white-space: nowrap;
519
-
520
- @media (max-width: 600px) {
521
- max-width: 80px;
522
- }
523
- }
561
+ const StyledOrderByBadge = styled.div `
562
+ display: inline-flex;
563
+ align-items: center;
564
+ gap: 4px;
565
+ padding: 3px 8px;
566
+ background: white;
567
+ border: 1px solid #d1d9e6;
568
+ border-radius: 16px;
569
+ font-size: 0.82rem;
570
+ color: #333;
571
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
572
+
573
+ .badge-name {
574
+ max-width: 120px;
575
+ overflow: hidden;
576
+ text-overflow: ellipsis;
577
+ white-space: nowrap;
578
+
579
+ @media (max-width: 600px) {
580
+ max-width: 80px;
581
+ }
582
+ }
524
583
  `;
525
- const StyledOrderByBadgeMore = styled.div `
526
- display: inline-flex;
527
- align-items: center;
528
- justify-content: center;
529
- padding: 3px 10px;
530
- background: linear-gradient(135deg, #4A96D2 0%, #2559A5 100%);
531
- border: none;
532
- border-radius: 16px;
533
- font-size: 0.82rem;
534
- font-weight: 600;
535
- color: white;
536
- cursor: pointer;
537
- transition: all 0.2s ease;
538
-
539
- &:hover {
540
- transform: scale(1.05);
541
- box-shadow: 0 2px 6px rgba(74, 150, 210, 0.4);
542
- }
584
+ const StyledOrderByBadgeMore = styled.div `
585
+ display: inline-flex;
586
+ align-items: center;
587
+ justify-content: center;
588
+ padding: 3px 10px;
589
+ background: linear-gradient(135deg, #4A96D2 0%, #2559A5 100%);
590
+ border: none;
591
+ border-radius: 16px;
592
+ font-size: 0.82rem;
593
+ font-weight: 600;
594
+ color: white;
595
+ cursor: pointer;
596
+ transition: all 0.2s ease;
597
+
598
+ &:hover {
599
+ transform: scale(1.05);
600
+ box-shadow: 0 2px 6px rgba(74, 150, 210, 0.4);
601
+ }
543
602
  `;
544
- const StyledOrderByEditButton = styled.div `
545
- display: flex;
546
- align-items: center;
547
- justify-content: center;
548
- width: 28px;
549
- height: 28px;
550
- border-radius: 4px;
551
- color: #4A96D2;
552
- cursor: pointer;
553
- transition: all 0.2s ease;
554
-
555
- &:hover {
556
- background-color: rgba(74, 150, 210, 0.15);
557
- }
603
+ const StyledOrderByEditButton = styled.div `
604
+ display: flex;
605
+ align-items: center;
606
+ justify-content: center;
607
+ width: 28px;
608
+ height: 28px;
609
+ border-radius: 4px;
610
+ color: #4A96D2;
611
+ cursor: pointer;
612
+ transition: all 0.2s ease;
613
+
614
+ &:hover {
615
+ background-color: rgba(74, 150, 210, 0.15);
616
+ }
558
617
  `;
559
618
  // =============================================================================
560
619
  // STYLED COMPONENTS per i badge degli output (select) - colore verde/teal
561
620
  // =============================================================================
562
- const StyledSelectBadgesContainer = styled.div `
563
- display: flex;
564
- flex-wrap: wrap;
565
- align-items: center;
566
- gap: 8px;
567
- padding: 6px 10px;
568
- margin: 1px 0;
569
- background: linear-gradient(135deg, rgba(46, 125, 50, 0.08) 0%, rgba(27, 94, 32, 0.08) 100%);
570
- border-radius: 8px;
571
- border: 1px solid rgba(46, 125, 50, 0.2);
621
+ const StyledSelectBadgesContainer = styled.div `
622
+ display: flex;
623
+ flex-wrap: wrap;
624
+ align-items: center;
625
+ gap: 8px;
626
+ padding: 6px 10px;
627
+ margin: 1px 0;
628
+ background: linear-gradient(135deg, rgba(46, 125, 50, 0.08) 0%, rgba(27, 94, 32, 0.08) 100%);
629
+ border-radius: 8px;
630
+ border: 1px solid rgba(46, 125, 50, 0.2);
572
631
  `;
573
- const StyledSelectLabel = styled.span `
574
- font-size: 0.82rem;
575
- font-weight: 600;
576
- color: #2E7D32;
577
- white-space: nowrap;
632
+ const StyledSelectLabel = styled.span `
633
+ font-size: 0.82rem;
634
+ font-weight: 600;
635
+ color: #2E7D32;
636
+ white-space: nowrap;
578
637
  `;
579
- const StyledSelectBadgesWrapper = styled.div `
580
- display: flex;
581
- flex-wrap: wrap;
582
- gap: 6px;
583
- align-items: center;
584
- flex: 1;
638
+ const StyledSelectBadgesWrapper = styled.div `
639
+ display: flex;
640
+ flex-wrap: wrap;
641
+ gap: 6px;
642
+ align-items: center;
643
+ flex: 1;
585
644
  `;
586
- const StyledSelectBadge = styled.div `
587
- display: inline-flex;
588
- align-items: center;
589
- gap: 4px;
590
- padding: 3px 8px;
591
- background: white;
592
- border: 1px solid #c8e6c9;
593
- border-radius: 16px;
594
- font-size: 0.82rem;
595
- color: #333;
596
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
597
-
598
- .badge-name {
599
- max-width: 120px;
600
- overflow: hidden;
601
- text-overflow: ellipsis;
602
- white-space: nowrap;
603
-
604
- @media (max-width: 600px) {
605
- max-width: 80px;
606
- }
607
- }
645
+ const StyledSelectBadge = styled.div `
646
+ display: inline-flex;
647
+ align-items: center;
648
+ gap: 4px;
649
+ padding: 3px 8px;
650
+ background: white;
651
+ border: 1px solid #c8e6c9;
652
+ border-radius: 16px;
653
+ font-size: 0.82rem;
654
+ color: #333;
655
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
656
+
657
+ .badge-name {
658
+ max-width: 120px;
659
+ overflow: hidden;
660
+ text-overflow: ellipsis;
661
+ white-space: nowrap;
662
+
663
+ @media (max-width: 600px) {
664
+ max-width: 80px;
665
+ }
666
+ }
608
667
  `;
609
- const StyledSelectBadgeMore = styled.div `
610
- display: inline-flex;
611
- align-items: center;
612
- justify-content: center;
613
- padding: 3px 10px;
614
- background: linear-gradient(135deg, #43A047 0%, #2E7D32 100%);
615
- border: none;
616
- border-radius: 16px;
617
- font-size: 0.82rem;
618
- font-weight: 600;
619
- color: white;
620
- cursor: pointer;
621
- transition: all 0.2s ease;
622
-
623
- &:hover {
624
- transform: scale(1.05);
625
- box-shadow: 0 2px 6px rgba(46, 125, 50, 0.4);
626
- }
668
+ const StyledSelectBadgeMore = styled.div `
669
+ display: inline-flex;
670
+ align-items: center;
671
+ justify-content: center;
672
+ padding: 3px 10px;
673
+ background: linear-gradient(135deg, #43A047 0%, #2E7D32 100%);
674
+ border: none;
675
+ border-radius: 16px;
676
+ font-size: 0.82rem;
677
+ font-weight: 600;
678
+ color: white;
679
+ cursor: pointer;
680
+ transition: all 0.2s ease;
681
+
682
+ &:hover {
683
+ transform: scale(1.05);
684
+ box-shadow: 0 2px 6px rgba(46, 125, 50, 0.4);
685
+ }
627
686
  `;
628
- const StyledSelectEditButton = styled.div `
629
- display: flex;
630
- align-items: center;
631
- justify-content: center;
632
- width: 28px;
633
- height: 28px;
634
- border-radius: 4px;
635
- color: #2E7D32;
636
- cursor: pointer;
637
- transition: all 0.2s ease;
638
-
639
- &:hover {
640
- background-color: rgba(46, 125, 50, 0.15);
641
- }
687
+ const StyledSelectEditButton = styled.div `
688
+ display: flex;
689
+ align-items: center;
690
+ justify-content: center;
691
+ width: 28px;
692
+ height: 28px;
693
+ border-radius: 4px;
694
+ color: #2E7D32;
695
+ cursor: pointer;
696
+ transition: all 0.2s ease;
697
+
698
+ &:hover {
699
+ background-color: rgba(46, 125, 50, 0.15);
700
+ }
642
701
  `;
643
- export const StyledToppyTextContainer = styled.div `
644
- padding: 22px 8px;
645
- width: 100%;
646
- max-width: 300px;
647
- border: 1px solid #2559A5;
648
- border-radius: 30px;
649
- display: flex;
650
- align-items: center;
651
- justify-content: center;
652
- box-sizing: border-box;
653
- min-height: 100px;
702
+ export const StyledToppyTextContainer = styled.div `
703
+ padding: 22px 8px;
704
+ width: 100%;
705
+ max-width: 300px;
706
+ border: 1px solid #2559A5;
707
+ border-radius: 30px;
708
+ display: flex;
709
+ align-items: center;
710
+ justify-content: center;
711
+ box-sizing: border-box;
712
+ min-height: 100px;
654
713
  `;
655
- export const StyledToppyText = styled.p `
656
- text-align: center;
657
- color: #2559A5;
658
- font-size: 1rem;
659
- user-select: none;
660
- -webkit-touch-callout: none;
661
- -webkit-user-select: none;
662
- -moz-user-select: none;
663
- -ms-user-select: none;
664
- margin: 0;
665
- display: -webkit-box;
666
- -webkit-box-orient: vertical;
667
- -webkit-line-clamp: 3;
668
- overflow: hidden;
669
- text-overflow: ellipsis;
670
- white-space: normal;
671
- word-break: break-word;
672
- line-height: 1.2;
714
+ export const StyledToppyText = styled.p `
715
+ text-align: center;
716
+ color: #2559A5;
717
+ font-size: 1rem;
718
+ user-select: none;
719
+ -webkit-touch-callout: none;
720
+ -webkit-user-select: none;
721
+ -moz-user-select: none;
722
+ -ms-user-select: none;
723
+ margin: 0;
724
+ display: -webkit-box;
725
+ -webkit-box-orient: vertical;
726
+ -webkit-line-clamp: 3;
727
+ overflow: hidden;
728
+ text-overflow: ellipsis;
729
+ white-space: normal;
730
+ word-break: break-word;
731
+ line-height: 1.2;
673
732
  `;
674
- export const StyledToppyImage = styled.img `
675
- width: 100%;
676
- max-width: 120px;
677
- height: auto;
678
- display: block;
733
+ export const StyledToppyImage = styled.img `
734
+ width: 100%;
735
+ max-width: 120px;
736
+ height: auto;
737
+ display: block;
679
738
  `;