From 743ddf298ec2df4db5e52e3b8c600f5eee5868ca Mon Sep 17 00:00:00 2001 From: IRBorisov <8611739+IRBorisov@users.noreply.github.com> Date: Sat, 26 Aug 2023 17:26:49 +0300 Subject: [PATCH] Implement subscriptions and improve UI --- rsconcept/backend/apps/rsform/admin.py | 9 +- .../apps/rsform/migrations/0001_initial.py | 27 ++-- rsconcept/backend/apps/rsform/models.py | 31 ++++ .../apps/rsform/tests/data/sample-rsform.trs | Bin 2752 -> 2848 bytes .../backend/apps/rsform/tests/t_models.py | 41 +++++- .../backend/apps/rsform/tests/t_views.py | 139 +++++++++++------- rsconcept/backend/apps/rsform/urls.py | 16 +- rsconcept/backend/apps/rsform/views.py | 30 +++- rsconcept/backend/apps/users/serializers.py | 26 ++-- rsconcept/backend/apps/users/tests/t_views.py | 27 +++- rsconcept/backend/apps/users/views.py | 28 +--- rsconcept/backend/fixtures/InitialData.json | 26 ++-- .../src/components/Common/TextArea.tsx | 14 +- .../src/components/Common/TextInput.tsx | 6 +- .../frontend/src/context/LibraryContext.tsx | 6 +- .../frontend/src/context/RSFormContext.tsx | 73 +++++++-- rsconcept/frontend/src/index.css | 4 + .../src/pages/LibraryPage/ViewLibrary.tsx | 40 +++-- .../src/pages/RSFormPage/EditorItems.tsx | 1 - .../src/pages/RSFormPage/EditorRSForm.tsx | 8 +- .../src/pages/RSFormPage/EditorTermGraph.tsx | 3 - .../frontend/src/pages/RSFormPage/RSTabs.tsx | 20 ++- .../src/pages/RSFormPage/RSTabsMenu.tsx | 20 ++- ...{ChangePassword.tsx => EditorPassword.tsx} | 67 +++++---- .../pages/UserProfilePage/EditorProfile.tsx | 68 +++++++++ .../src/pages/UserProfilePage/UserProfile.tsx | 74 ---------- .../src/pages/UserProfilePage/UserTabs.tsx | 48 +++++- .../UserProfilePage/ViewSubscriptions.tsx | 67 +++++++++ rsconcept/frontend/src/utils/backendAPI.ts | 50 ++++--- rsconcept/frontend/src/utils/constants.ts | 1 + rsconcept/frontend/src/utils/models.ts | 5 +- 31 files changed, 672 insertions(+), 303 deletions(-) rename rsconcept/frontend/src/pages/UserProfilePage/{ChangePassword.tsx => EditorPassword.tsx} (51%) create mode 100644 rsconcept/frontend/src/pages/UserProfilePage/EditorProfile.tsx delete mode 100644 rsconcept/frontend/src/pages/UserProfilePage/UserProfile.tsx create mode 100644 rsconcept/frontend/src/pages/UserProfilePage/ViewSubscriptions.tsx diff --git a/rsconcept/backend/apps/rsform/admin.py b/rsconcept/backend/apps/rsform/admin.py index b4d370d6..2d95688e 100644 --- a/rsconcept/backend/apps/rsform/admin.py +++ b/rsconcept/backend/apps/rsform/admin.py @@ -8,9 +8,14 @@ class ConstituentaAdmin(admin.ModelAdmin): ''' Admin model: Constituenta. ''' -class Librarydmin(admin.ModelAdmin): +class LibraryAdmin(admin.ModelAdmin): ''' Admin model: LibraryItem. ''' +class SubscriptionAdmin(admin.ModelAdmin): + ''' Admin model: Subscriptions. ''' + + admin.site.register(models.Constituenta, ConstituentaAdmin) -admin.site.register(models.LibraryItem, Librarydmin) +admin.site.register(models.LibraryItem, LibraryAdmin) +admin.site.register(models.Subscription, SubscriptionAdmin) diff --git a/rsconcept/backend/apps/rsform/migrations/0001_initial.py b/rsconcept/backend/apps/rsform/migrations/0001_initial.py index 3ab27367..1aff9c2f 100644 --- a/rsconcept/backend/apps/rsform/migrations/0001_initial.py +++ b/rsconcept/backend/apps/rsform/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.4 on 2023-08-25 12:15 +# Generated by Django 4.2.4 on 2023-08-26 10:09 import apps.rsform.models from django.conf import settings @@ -35,18 +35,6 @@ class Migration(migrations.Migration): 'verbose_name_plural': 'Схемы', }, ), - migrations.CreateModel( - name='Subscription', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rsform.libraryitem', verbose_name='Элемент')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Пользователь')), - ], - options={ - 'verbose_name': 'Подписки', - 'verbose_name_plural': 'Подписка', - }, - ), migrations.CreateModel( name='Constituenta', fields=[ @@ -68,4 +56,17 @@ class Migration(migrations.Migration): 'verbose_name_plural': 'Конституенты', }, ), + migrations.CreateModel( + name='Subscription', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rsform.libraryitem', verbose_name='Элемент')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Пользователь')), + ], + options={ + 'verbose_name': 'Подписки', + 'verbose_name_plural': 'Подписка', + 'unique_together': {('user', 'item')}, + }, + ), ] diff --git a/rsconcept/backend/apps/rsform/models.py b/rsconcept/backend/apps/rsform/models.py index 6fac23de..c631a04c 100644 --- a/rsconcept/backend/apps/rsform/models.py +++ b/rsconcept/backend/apps/rsform/models.py @@ -127,6 +127,17 @@ class LibraryItem(Model): def get_absolute_url(self): return f'/api/library/{self.pk}/' + def subscribers(self) -> list[User]: + ''' Get all subscribers for this item . ''' + return [s.user for s in Subscription.objects.filter(item=self.pk)] + + @transaction.atomic + def save(self, *args, **kwargs): + subscribe = not self.pk and self.owner + super().save(*args, **kwargs) + if subscribe: + Subscription.subscribe(user=self.owner, item=self) + class Subscription(Model): ''' User subscription to library item. ''' @@ -145,10 +156,28 @@ class Subscription(Model): ''' Model metadata. ''' verbose_name = 'Подписки' verbose_name_plural = 'Подписка' + unique_together = [['user', 'item']] def __str__(self) -> str: return f'{self.user} -> {self.item}' + @staticmethod + def subscribe(user: User, item: LibraryItem) -> bool: + ''' Add subscription. ''' + if Subscription.objects.filter(user=user, item=item).exists(): + return False + Subscription.objects.create(user=user, item=item) + return True + + @staticmethod + def unsubscribe(user: User, item: LibraryItem) -> bool: + ''' Remove subscription. ''' + sub = Subscription.objects.filter(user=user, item=item) + if not sub.exists(): + return False + sub.delete() + return True + class Constituenta(Model): ''' Constituenta is the base unit for every conceptual schema ''' @@ -514,6 +543,7 @@ class PyConceptAdapter: result['time_update'] = self.schema.item.time_update result['time_create'] = self.schema.item.time_create result['is_common'] = self.schema.item.is_common + result['is_canonical'] = self.schema.item.is_canonical result['owner'] = (self.schema.item.owner.pk if self.schema.item.owner is not None else None) for cst_data in result['items']: cst = Constituenta.objects.get(pk=cst_data['id']) @@ -527,6 +557,7 @@ class PyConceptAdapter: 'raw': cst.definition_raw, 'resolved': cst.definition_resolved, } + result['subscribers'] = [item.pk for item in self.schema.item.subscribers()] return result def _prepare_request(self) -> dict: diff --git a/rsconcept/backend/apps/rsform/tests/data/sample-rsform.trs b/rsconcept/backend/apps/rsform/tests/data/sample-rsform.trs index 3d1c3a475735cbd6e3ac984d02b63feb60c5c48e..fb49cdd85ca5d47cff150627489aaf5ab0f89806 100644 GIT binary patch literal 2848 zcmZ{m=Tj337R8eQ(xpS_1PG$^-g_@1NH4s?<|ZVh%m4s@0$>8-u&o3Gk0Pi506iA~fcAIm z>H`n*a`O%pM+EqIA2{r#Qd=!O`|B z`AV_vH=flZL8B+WUk!uh_iAqPRdTHAwt|Cej!heAt4*;8`@IVs>6q8Ri^4;g487MM~PPVLZmTLf8WzI*&cZ^HyozPUx3C8 z9)0w&<>C`r$z>e}IlaN>O-PC-)jM>lzladbI3LNaM$`=naEFD@W*}+v5D=Y}GD*)jfI*?|%*ILeYgtoqcubG8(kuael_72{H9s725 z9)V#+Lc%X(oTiI274;pX%=;PiMsvN9RFFxg2Ns9V5Px{pBYL0m9wNI`9@zA2A9v)v z$YWgauai{#((KK*S(`ZLU?V!o>l+mLy7)X#v#Swp!pG!czu+sbKhE|JV^HBAuH0qWftz+F23SPh7 zKNeKb+|$>x1F*d$Qiw^!l_C=A5X$W39cj-#Qrlv}i~5h3t06MV)!0@H3_#%eCPOpZ z?*2`9NhD?h-?o7{xM+TEdv%|(?#ncK+lU**DNi)AZ>Hsol;C_jbds3W>t%2tb^+cli@%r5|CFpYBt}(4|mmL zlsF$oN*vRLigs^f*nF>4{7w|D+RV;)2j1r=87O!ytAB70J(~NOx896+_1rPSay{(g z;PbfgaYoXhq9UAmU?yH$P%59xuwZ!njFoh{qdjyX>rpU=qD{X$eQH@sEBde%~FlbF(|a?My@ zfYpe=4K(gt)6V(enq9~PB5q#YTLrF153gE8{sL$F`y{uxPvfE*#oEvJQDa^%EHEis zUD#d6=-7H8lbEVFT^@Pl^Dw}NUWGVuLdt^QWcqhqgIlPQPX-1f#K2O0*#Su-!$F*L z(n;{m)V+6Vr1s)~kAFEk#)i@K0f){#;1w1@ib=uTQQ)y~>gJHVQY9l@EFV=@wUYl37%fGj% zxEH+$dVP~S=+ouXQE9E?`j!~heM+pP!3v(aBKJ~O^7i|a_20#iwABVbG*BxhF)4>P z*5-xa{u-MHRxJj*W2Aynp`!TrZqqm~zxO)kxM5Y)f~-{BpFZYvuY+4OglRFGC)6UNYz*n2u472n>?15M2 zv+svFU3piF+PdHW)Yc4W8;9f%`H|}<5j$^`(d7EK!h5Y|47P!@(w5ehdvlBU!W|B0 zGoaxVh6$YgrKq_heQ9@dD!k=XMWW{%snvV-BHXjkN8Vyo+D%ujEcZMow3Tc&`$IeZ z1}4sN;-whN6yvI@O9}G%7|&Yjx8i{$V;W2O(zMQfAl*S?ei@B&6PO7JnsGuj@)ZO2 zpRXoK)L_?j%x12Tc7#~|*w4{vI*x!}bbWDgqU!y$2Cuvgb2;XOc=j83wB_H_24uq* z#ex$%pV%lZXdU<`@yt_*b(+G3MD8;tzbsU7Z+a20FL-!tFdU^EA_d}tv(8YdBr&d&9yL^FH`D1oJy!+SoQGJEbEn_C~k z3D5YRJkH9___MURjQvT5?AwTLS>o@j?ukA}g<{IRD(t;I*Bw@92ydFf<@Idz4ZX^3 zO@0UIAnu2&I^`DyL+-aX62iJ7LKKefU~*&lbo6FtNp{wfBYK8uD79|4n^HFW zSEKAN{1&Y$W zfHVGhUGuA+=E~|CoUJLWx#za~_+D7P)UR}_x->sy>YT?h_&~3EI8t)?GX3r9G5@EC4{S!XLTKEt7J+VjME0>O0c(Xm`Lxwt09n^n*c!?fd2=pg1>R~ vpW*xW{vULu`cDl6fB_`&?ln2{c;J8j{%htYWVikaz~6HE?bW|I7y$SeGUZQ~ literal 2752 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/', views.ConstituentAPIView.as_view(), name='constituenta-detail'), - path('rsforms/import-trs/', views.TrsImportView.as_view()), - path('rsforms/create-detailed/', views.create_rsform), - path('func/parse-expression/', views.parse_expression), - path('func/to-ascii/', views.convert_to_ascii), - path('func/to-math/', views.convert_to_math), + path('library/active', views.LibraryActiveView.as_view(), name='library'), + path('constituents/', views.ConstituentAPIView.as_view(), name='constituenta-detail'), + path('rsforms/import-trs', views.TrsImportView.as_view()), + path('rsforms/create-detailed', views.create_rsform), + path('func/parse-expression', views.parse_expression), + path('func/to-ascii', views.convert_to_ascii), + path('func/to-math', views.convert_to_math), path('', include(library_router.urls)), ] diff --git a/rsconcept/backend/apps/rsform/views.py b/rsconcept/backend/apps/rsform/views.py index 9dd0e311..7d45d86a 100644 --- a/rsconcept/backend/apps/rsform/views.py +++ b/rsconcept/backend/apps/rsform/views.py @@ -24,7 +24,8 @@ class LibraryActiveView(generics.ListAPIView): def get_queryset(self): user = self.request.user if not user.is_anonymous: - return m.LibraryItem.objects.filter(Q(is_common=True) | Q(owner=user)) + # pyling: disable=unsupported-binary-operation + return m.LibraryItem.objects.filter(Q(is_common=True) | Q(owner=user) | Q(subscription__user=user)) else: return m.LibraryItem.objects.filter(is_common=True) @@ -62,7 +63,7 @@ class LibraryViewSet(viewsets.ModelViewSet): def get_permissions(self): if self.action in ['update', 'destroy', 'partial_update']: permission_classes = [utils.ObjectOwnerOrAdmin] - elif self.action in ['create', 'clone']: + elif self.action in ['create', 'clone', 'subscribe', 'unsubscribe']: permission_classes = [permissions.IsAuthenticated] elif self.action in ['claim']: permission_classes = [utils.IsClaimable] @@ -70,13 +71,16 @@ class LibraryViewSet(viewsets.ModelViewSet): permission_classes = [permissions.AllowAny] return [permission() for permission in permission_classes] + def _get_item(self) -> m.LibraryItem: + return cast(m.LibraryItem, self.get_object()) + @transaction.atomic @action(detail=True, methods=['post'], url_path='clone') def clone(self, request, pk): ''' Endpoint: Create deep copy of library item. ''' serializer = s.LibraryItemSerializer(data=request.data) serializer.is_valid(raise_exception=True) - item = cast(m.LibraryItem, self.get_object()) + item = self._get_item() if item.item_type == m.LibraryItemType.RSFORM: schema = m.RSForm(item) clone_data = s.RSFormTRSSerializer(schema).data @@ -93,21 +97,37 @@ class LibraryViewSet(viewsets.ModelViewSet): return Response(status=201, data=m.PyConceptAdapter(new_schema).full()) return Response(status=404) + @transaction.atomic @action(detail=True, methods=['post']) def claim(self, request, pk=None): ''' Endpoint: Claim ownership of LibraryItem. ''' - item = cast(m.LibraryItem, self.get_object()) + item = self._get_item() if item.owner == self.request.user: return Response(status=304) else: item.owner = self.request.user item.save() + m.Subscription.subscribe(user=item.owner, item=item) return Response(status=200, data=s.LibraryItemSerializer(item).data) + @action(detail=True, methods=['post']) + def subscribe(self, request, pk): + ''' Endpoint: Subscribe current user to item. ''' + item = self._get_item() + m.Subscription.subscribe(user=self.request.user, item=item) + return Response(status=200) + + @action(detail=True, methods=['delete']) + def unsubscribe(self, request, pk): + ''' Endpoint: Unsubscribe current user from item. ''' + item = self._get_item() + m.Subscription.unsubscribe(user=self.request.user, item=item) + return Response(status=200) + class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.RetrieveAPIView): ''' Endpoint: RSForm operations. ''' - queryset = m.LibraryItem.objects.all().filter(item_type=m.LibraryItemType.RSFORM) + queryset = m.LibraryItem.objects.filter(item_type=m.LibraryItemType.RSFORM) serializer_class = s.LibraryItemSerializer def _get_schema(self) -> m.RSForm: diff --git a/rsconcept/backend/apps/users/serializers.py b/rsconcept/backend/apps/users/serializers.py index 30ad8170..6ddc6618 100644 --- a/rsconcept/backend/apps/users/serializers.py +++ b/rsconcept/backend/apps/users/serializers.py @@ -3,6 +3,7 @@ from django.contrib.auth import authenticate from django.contrib.auth.password_validation import validate_password from rest_framework import serializers +from apps.rsform.models import Subscription from . import models @@ -40,16 +41,23 @@ class LoginSerializer(serializers.Serializer): raise NotImplementedError('unexpected `update()` call') -class AuthSerializer(serializers.ModelSerializer): +class AuthSerializer(serializers.Serializer): ''' Serializer: Authentication data. ''' - class Meta: - ''' serializer metadata. ''' - model = models.User - fields = [ - 'id', - 'username', - 'is_staff' - ] + def to_representation(self, instance: models.User) -> dict: + if instance.is_anonymous: + return { + 'id': None, + 'username': '', + 'is_staff': False, + 'subscriptions': [] + } + else: + return { + 'id': instance.pk, + 'username': instance.username, + 'is_staff': instance.is_staff, + 'subscriptions': [sub.item.pk for sub in Subscription.objects.filter(user=instance)] + } class UserInfoSerializer(serializers.ModelSerializer): diff --git a/rsconcept/backend/apps/users/tests/t_views.py b/rsconcept/backend/apps/users/tests/t_views.py index 6187b844..1f755911 100644 --- a/rsconcept/backend/apps/users/tests/t_views.py +++ b/rsconcept/backend/apps/users/tests/t_views.py @@ -3,9 +3,10 @@ import json from rest_framework.test import APITestCase, APIClient from apps.users.models import User +from apps.rsform.models import LibraryItem, LibraryItemType -# TODO: test AUTH and ATIVE_USERS +# TODO: test ACTIVE_USERS class TestUserAPIViews(APITestCase): def setUp(self): self.username = 'UserTest' @@ -30,6 +31,30 @@ class TestUserAPIViews(APITestCase): self.assertEqual(self.client.post('/users/api/logout').status_code, 403) + def test_auth(self): + LibraryItem.objects.create(item_type=LibraryItemType.RSFORM, title='T1') + item = LibraryItem.objects.create( + item_type=LibraryItemType.RSFORM, + title='Test', + alias='T1', + is_common=True, + owner=self.user + ) + response = self.client.get('/users/api/auth') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data['id'], None) + self.assertEqual(response.data['username'], '') + self.assertEqual(response.data['is_staff'], False) + self.assertEqual(response.data['subscriptions'], []) + + self.client.force_login(self.user) + response = self.client.get('/users/api/auth') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data['id'], self.user.pk) + self.assertEqual(response.data['username'], self.user.username) + self.assertEqual(response.data['is_staff'], self.user.is_staff) + self.assertEqual(response.data['subscriptions'], [item.pk]) + class TestUserUserProfileAPIView(APITestCase): def setUp(self): diff --git a/rsconcept/backend/apps/users/views.py b/rsconcept/backend/apps/users/views.py index 56055cb8..6cd097e8 100644 --- a/rsconcept/backend/apps/users/views.py +++ b/rsconcept/backend/apps/users/views.py @@ -8,9 +8,7 @@ from . import serializers from . import models class LoginAPIView(views.APIView): - ''' - Endpoint: Login user via username + password. - ''' + ''' Endpoint: Login via username + password. ''' permission_classes = (permissions.AllowAny,) def post(self, request): @@ -25,9 +23,7 @@ class LoginAPIView(views.APIView): class LogoutAPIView(views.APIView): - ''' - Endpoint: Logout current user. - ''' + ''' Endpoint: Logout. ''' permission_classes = (permissions.IsAuthenticated,) def post(self, request): @@ -36,17 +32,13 @@ class LogoutAPIView(views.APIView): class SignupAPIView(generics.CreateAPIView): - ''' - Register user. - ''' + ''' Endpoint: Register user. ''' permission_classes = (permissions.AllowAny, ) serializer_class = serializers.SignupSerializer class AuthAPIView(generics.RetrieveAPIView): - ''' - Get current user authentification ID. - ''' + ''' Endpoint: Current user info. ''' permission_classes = (permissions.AllowAny,) serializer_class = serializers.AuthSerializer @@ -55,9 +47,7 @@ class AuthAPIView(generics.RetrieveAPIView): class ActiveUsersView(generics.ListAPIView): - ''' - Endpoint: Get list of active users. - ''' + ''' Endpoint: Get list of active users. ''' permission_classes = (permissions.AllowAny,) serializer_class = serializers.UserSerializer @@ -66,9 +56,7 @@ class ActiveUsersView(generics.ListAPIView): class UserProfileAPIView(generics.RetrieveUpdateAPIView): - ''' - Endpoint: User profile info. - ''' + ''' Endpoint: User profile. ''' permission_classes = (permissions.IsAuthenticated,) serializer_class = serializers.UserSerializer @@ -77,9 +65,7 @@ class UserProfileAPIView(generics.RetrieveUpdateAPIView): class UpdatePassword(views.APIView): - ''' - Endpoint: Change password for current user. - ''' + ''' Endpoint: Change password for current user. ''' permission_classes = (permissions.IsAuthenticated, ) def get_object(self, queryset=None): diff --git a/rsconcept/backend/fixtures/InitialData.json b/rsconcept/backend/fixtures/InitialData.json index 3c07aa53..8fbeddf6 100644 --- a/rsconcept/backend/fixtures/InitialData.json +++ b/rsconcept/backend/fixtures/InitialData.json @@ -159,7 +159,7 @@ "alias": "M0005", "comment": "", "is_common": true, - "is_canonical": true, + "is_canonical": false, "time_create": "2023-08-25T19:03:40.052Z", "time_update": "2023-08-25T19:03:40.052Z" } @@ -174,7 +174,7 @@ "alias": "M0006", "comment": "", "is_common": true, - "is_canonical": true, + "is_canonical": false, "time_create": "2023-08-25T19:03:40.066Z", "time_update": "2023-08-25T19:03:40.066Z" } @@ -189,7 +189,7 @@ "alias": "M0007", "comment": "", "is_common": true, - "is_canonical": true, + "is_canonical": false, "time_create": "2023-08-25T19:03:40.077Z", "time_update": "2023-08-25T19:03:40.077Z" } @@ -204,7 +204,7 @@ "alias": "M0008", "comment": "", "is_common": true, - "is_canonical": true, + "is_canonical": false, "time_create": "2023-08-25T19:03:40.081Z", "time_update": "2023-08-25T19:03:40.081Z" } @@ -219,7 +219,7 @@ "alias": "M0009", "comment": "", "is_common": true, - "is_canonical": true, + "is_canonical": false, "time_create": "2023-08-25T19:03:40.094Z", "time_update": "2023-08-25T19:03:40.095Z" } @@ -234,7 +234,7 @@ "alias": "M0010", "comment": "", "is_common": true, - "is_canonical": true, + "is_canonical": false, "time_create": "2023-08-25T19:03:40.118Z", "time_update": "2023-08-25T19:03:40.118Z" } @@ -249,7 +249,7 @@ "alias": "M0011", "comment": "", "is_common": true, - "is_canonical": true, + "is_canonical": false, "time_create": "2023-08-25T19:03:40.132Z", "time_update": "2023-08-25T19:03:40.132Z" } @@ -264,7 +264,7 @@ "alias": "M0012", "comment": "", "is_common": true, - "is_canonical": true, + "is_canonical": false, "time_create": "2023-08-25T19:03:40.152Z", "time_update": "2023-08-25T19:03:40.152Z" } @@ -279,7 +279,7 @@ "alias": "M0013", "comment": "", "is_common": true, - "is_canonical": true, + "is_canonical": false, "time_create": "2023-08-25T19:03:40.167Z", "time_update": "2023-08-25T19:03:40.168Z" } @@ -294,7 +294,7 @@ "alias": "M0014", "comment": "", "is_common": true, - "is_canonical": true, + "is_canonical": false, "time_create": "2023-08-25T19:03:40.196Z", "time_update": "2023-08-25T19:03:40.196Z" } @@ -309,7 +309,7 @@ "alias": "M0015", "comment": "", "is_common": true, - "is_canonical": true, + "is_canonical": false, "time_create": "2023-08-25T19:03:40.207Z", "time_update": "2023-08-25T19:03:40.207Z" } @@ -324,7 +324,7 @@ "alias": "M0016", "comment": "", "is_common": true, - "is_canonical": true, + "is_canonical": false, "time_create": "2023-08-25T19:03:40.258Z", "time_update": "2023-08-25T19:03:40.258Z" } @@ -354,7 +354,7 @@ "alias": "D0002", "comment": "", "is_common": true, - "is_canonical": true, + "is_canonical": false, "time_create": "2023-08-25T19:03:40.459Z", "time_update": "2023-08-25T19:03:40.460Z" } diff --git a/rsconcept/frontend/src/components/Common/TextArea.tsx b/rsconcept/frontend/src/components/Common/TextArea.tsx index 2b7ffd9b..d2158ef2 100644 --- a/rsconcept/frontend/src/components/Common/TextArea.tsx +++ b/rsconcept/frontend/src/components/Common/TextArea.tsx @@ -3,14 +3,15 @@ import { TextareaHTMLAttributes } from 'react'; import Label from './Label'; export interface TextAreaProps -extends Omit, 'className'> { +extends Omit, 'className' | 'title'> { label: string + tooltip?: string widthClass?: string colorClass?: string } function TextArea({ - id, label, required, + id, label, required, tooltip, widthClass = 'w-full', colorClass = 'clr-input', rows = 4, @@ -24,10 +25,11 @@ function TextArea({ htmlFor={id} />