From d909151ddcbbf14aadb3e1759b10ffb03c7eaa66 Mon Sep 17 00:00:00 2001
From: Monnot <48322244+Saigai@users.noreply.github.com>
Date: Sun, 3 Jan 2021 17:41:10 +0100
Subject: [PATCH] changed classes for fragment in TD7 by creating those classes
 via Fragment in New menu. Created actions to navigate and partial
 implementation of log in fragment.

---
 app/build.gradle                              |  3 +-
 app/src/main/AndroidManifest.xml              |  6 +-
 .../AuthentificationActivity.kt               |  2 +-
 .../network/AuthentificationFragment.kt       | 32 ++++++++
 .../clemhaowen/dm_td2/network/Constants.kt    |  3 +
 .../clemhaowen/dm_td2/network/LoginForm.kt    | 12 +++
 .../dm_td2/network/LoginFragment.kt           | 76 +++++++++++++++++++
 .../dm_td2/network/LoginResponse.kt           | 10 +++
 .../dm_td2/network/SignupFragment.kt          | 21 +++++
 .../clemhaowen/dm_td2/network/UserService.kt  |  8 +-
 .../dm_td2/tasklist/TaskListFragment.kt       | 18 +++--
 .../userInfo/AuthentificationFragment.kt      |  4 -
 .../dm_td2/userInfo/LoginFragment.kt          |  4 -
 .../dm_td2/userInfo/SignupFragment.kt         |  4 -
 ...ment.xml => fragment_authentification.xml} |  7 +-
 ...{login_fragment.xml => fragment_login.xml} |  3 +
 ...ignup_fragment.xml => fragment_signup.xml} |  3 +
 app/src/main/res/navigation/nav_graph.xml     | 28 ++++++-
 app/src/main/res/values/strings.xml           |  2 +
 build.gradle                                  |  3 +-
 20 files changed, 215 insertions(+), 34 deletions(-)
 rename app/src/main/java/com/clemhaowen/dm_td2/{userInfo => network}/AuthentificationActivity.kt (89%)
 create mode 100644 app/src/main/java/com/clemhaowen/dm_td2/network/AuthentificationFragment.kt
 create mode 100644 app/src/main/java/com/clemhaowen/dm_td2/network/Constants.kt
 create mode 100644 app/src/main/java/com/clemhaowen/dm_td2/network/LoginForm.kt
 create mode 100644 app/src/main/java/com/clemhaowen/dm_td2/network/LoginFragment.kt
 create mode 100644 app/src/main/java/com/clemhaowen/dm_td2/network/LoginResponse.kt
 create mode 100644 app/src/main/java/com/clemhaowen/dm_td2/network/SignupFragment.kt
 delete mode 100644 app/src/main/java/com/clemhaowen/dm_td2/userInfo/AuthentificationFragment.kt
 delete mode 100644 app/src/main/java/com/clemhaowen/dm_td2/userInfo/LoginFragment.kt
 delete mode 100644 app/src/main/java/com/clemhaowen/dm_td2/userInfo/SignupFragment.kt
 rename app/src/main/res/layout/{authentification_fragment.xml => fragment_authentification.xml} (75%)
 rename app/src/main/res/layout/{login_fragment.xml => fragment_login.xml} (90%)
 rename app/src/main/res/layout/{signup_fragment.xml => fragment_signup.xml} (94%)

diff --git a/app/build.gradle b/app/build.gradle
index 87ab9d3..993940b 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -2,12 +2,12 @@ plugins {
     id 'com.android.application'
     id 'kotlin-android'
     id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlin_version" // vérifier que votre version de kotlin est la même
+    id 'androidx.navigation.safeargs.kotlin'
 }
 
 android {
     compileSdkVersion 30
     buildToolsVersion "30.0.2"
-
     defaultConfig {
         applicationId "com.clemhaowen.dm_td2"
         minSdkVersion 23
@@ -43,6 +43,7 @@ dependencies {
     implementation 'com.google.android.material:material:1.0.0'
     implementation 'androidx.navigation:navigation-fragment-ktx:2.2.2'
     implementation 'androidx.navigation:navigation-ui-ktx:2.2.2'
+    implementation 'androidx.legacy:legacy-support-v4:1.0.0'
     testImplementation 'junit:junit:4.+'
    // androidTestImplementation 'com.android.support.test:runner:1.0.2'
     androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 70ac1d6..e8b8e56 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -13,7 +13,7 @@
         android:roundIcon="@mipmap/ic_launcher_round"
         android:supportsRtl="true"
         android:theme="@style/Theme.DM_td2">
-        <activity android:name=".userInfo.AuthentificationActivity">
+        <activity android:name=".network.AuthentificationActivity">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
 
@@ -34,11 +34,7 @@
 
         <activity android:name=".task.TaskActivity" />
         <activity android:name=".MainActivity">
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
 
-                <category android:name="android.intent.category.LAUNCHER" />
-            </intent-filter>
         </activity>
     </application>
 
diff --git a/app/src/main/java/com/clemhaowen/dm_td2/userInfo/AuthentificationActivity.kt b/app/src/main/java/com/clemhaowen/dm_td2/network/AuthentificationActivity.kt
similarity index 89%
rename from app/src/main/java/com/clemhaowen/dm_td2/userInfo/AuthentificationActivity.kt
rename to app/src/main/java/com/clemhaowen/dm_td2/network/AuthentificationActivity.kt
index dfb4809..59606f1 100644
--- a/app/src/main/java/com/clemhaowen/dm_td2/userInfo/AuthentificationActivity.kt
+++ b/app/src/main/java/com/clemhaowen/dm_td2/network/AuthentificationActivity.kt
@@ -1,4 +1,4 @@
-package com.clemhaowen.dm_td2.userInfo
+package com.clemhaowen.dm_td2.network
 
 import androidx.appcompat.app.AppCompatActivity
 import android.os.Bundle
diff --git a/app/src/main/java/com/clemhaowen/dm_td2/network/AuthentificationFragment.kt b/app/src/main/java/com/clemhaowen/dm_td2/network/AuthentificationFragment.kt
new file mode 100644
index 0000000..7337cdc
--- /dev/null
+++ b/app/src/main/java/com/clemhaowen/dm_td2/network/AuthentificationFragment.kt
@@ -0,0 +1,32 @@
+package com.clemhaowen.dm_td2.network
+
+import android.os.Bundle
+import androidx.fragment.app.Fragment
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Button
+import androidx.navigation.fragment.findNavController
+import com.clemhaowen.dm_td2.R
+
+class AuthentificationFragment : Fragment() {
+
+    override fun onCreateView(
+        inflater: LayoutInflater, container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        // Inflate the layout for this fragment
+        return inflater.inflate(R.layout.fragment_authentification, container, false)
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        val logInButton = view.findViewById<Button>(R.id.login_button)
+        logInButton.setOnClickListener {
+            findNavController().navigate(R.id.action_authentificationFragment_to_loginFragment)
+        }
+        val signUpButton = view.findViewById<Button>(R.id.signup_button)
+        signUpButton.setOnClickListener {
+            findNavController().navigate(R.id.action_authentificationFragment_to_signupFragment)
+        }
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/clemhaowen/dm_td2/network/Constants.kt b/app/src/main/java/com/clemhaowen/dm_td2/network/Constants.kt
new file mode 100644
index 0000000..74e4414
--- /dev/null
+++ b/app/src/main/java/com/clemhaowen/dm_td2/network/Constants.kt
@@ -0,0 +1,3 @@
+package com.clemhaowen.dm_td2.network
+
+const val SHARED_PREF_TOKEN_KEY = "auth_token_key"
\ No newline at end of file
diff --git a/app/src/main/java/com/clemhaowen/dm_td2/network/LoginForm.kt b/app/src/main/java/com/clemhaowen/dm_td2/network/LoginForm.kt
new file mode 100644
index 0000000..6e757fe
--- /dev/null
+++ b/app/src/main/java/com/clemhaowen/dm_td2/network/LoginForm.kt
@@ -0,0 +1,12 @@
+package com.clemhaowen.dm_td2.network
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class LoginForm(
+    @SerialName("email")
+    val email : String,
+    @SerialName("password")
+    val password : String
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/clemhaowen/dm_td2/network/LoginFragment.kt b/app/src/main/java/com/clemhaowen/dm_td2/network/LoginFragment.kt
new file mode 100644
index 0000000..047aed8
--- /dev/null
+++ b/app/src/main/java/com/clemhaowen/dm_td2/network/LoginFragment.kt
@@ -0,0 +1,76 @@
+package com.clemhaowen.dm_td2.network
+
+import android.os.Bundle
+import androidx.fragment.app.Fragment
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Button
+import android.widget.EditText
+import android.widget.Toast
+import androidx.appcompat.app.AlertDialog
+import androidx.core.content.edit
+import androidx.lifecycle.lifecycleScope
+import androidx.preference.PreferenceManager
+import com.clemhaowen.dm_td2.R
+import com.clemhaowen.dm_td2.network.LoginForm
+import kotlinx.coroutines.launch
+
+class LoginFragment : Fragment() {
+    private val userService = Api.userService
+
+    override fun onCreateView(
+        inflater: LayoutInflater, container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        // Inflate the layout for this fragment
+        return inflater.inflate(R.layout.fragment_login, container, false)
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        val loginButton = view.findViewById<Button>(R.id.validate_login_button)
+        loginButton.setOnClickListener {
+            val emailEditText = view.findViewById<EditText>(R.id.editTextTextEmailAddress)
+            val pwdEditText = view.findViewById<EditText>(R.id.editTextTextPassword)
+            if (emailEditText.text.isNotEmpty() && pwdEditText.text.isNotEmpty()){
+                val loginForm = LoginForm(email = emailEditText.text.toString(), password = pwdEditText.text.toString())
+                lifecycleScope.launch {
+                    val loginResponse = userService.login(loginForm)
+                    if (loginResponse.isSuccessful){
+                        PreferenceManager.getDefaultSharedPreferences(context).edit {
+                            putString(SHARED_PREF_TOKEN_KEY, loginResponse.body()?.token)
+                        }
+                        AlertDialog.Builder(context!!).apply {
+                            setMessage("Logged in.")
+                            setPositiveButton("OK") { _, _ ->
+                            }
+                            setCancelable(true)
+                            show()
+                        }
+                    }
+                    else{
+                        Toast.makeText(context, "Your email or password is incorrect.\nIf you don't have an account please sign up first.", Toast.LENGTH_LONG).show()
+                        AlertDialog.Builder(context!!).apply {
+                            setMessage("Your email or password is incorrect.\n" +
+                                    "If you don't have an account please sign up first.")
+                            setPositiveButton("OK") { _, _ ->
+                            }
+                            setCancelable(true)
+                            show()
+                        }
+                    }
+                }
+
+            }
+            else {
+                AlertDialog.Builder(context!!).apply {
+                    setMessage("Please fill in the 2 blanks.")
+                    setPositiveButton("OK") { _, _ ->
+                    }
+                    setCancelable(true)
+                    show()
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/clemhaowen/dm_td2/network/LoginResponse.kt b/app/src/main/java/com/clemhaowen/dm_td2/network/LoginResponse.kt
new file mode 100644
index 0000000..492458c
--- /dev/null
+++ b/app/src/main/java/com/clemhaowen/dm_td2/network/LoginResponse.kt
@@ -0,0 +1,10 @@
+package com.clemhaowen.dm_td2.network
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class LoginResponse (
+    @SerialName("token")
+    val token : String
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/clemhaowen/dm_td2/network/SignupFragment.kt b/app/src/main/java/com/clemhaowen/dm_td2/network/SignupFragment.kt
new file mode 100644
index 0000000..e11d326
--- /dev/null
+++ b/app/src/main/java/com/clemhaowen/dm_td2/network/SignupFragment.kt
@@ -0,0 +1,21 @@
+package com.clemhaowen.dm_td2.network
+
+import android.os.Bundle
+import androidx.fragment.app.Fragment
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.clemhaowen.dm_td2.R
+
+
+
+class SignupFragment : Fragment() {
+
+    override fun onCreateView(
+        inflater: LayoutInflater, container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        // Inflate the layout for this fragment
+        return inflater.inflate(R.layout.fragment_signup, container, false)
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/clemhaowen/dm_td2/network/UserService.kt b/app/src/main/java/com/clemhaowen/dm_td2/network/UserService.kt
index 7306bc4..0e41b8b 100644
--- a/app/src/main/java/com/clemhaowen/dm_td2/network/UserService.kt
+++ b/app/src/main/java/com/clemhaowen/dm_td2/network/UserService.kt
@@ -2,10 +2,7 @@ package com.clemhaowen.dm_td2.network
 
 import okhttp3.MultipartBody
 import retrofit2.Response
-import retrofit2.http.GET
-import retrofit2.http.Multipart
-import retrofit2.http.PATCH
-import retrofit2.http.Part
+import retrofit2.http.*
 
 interface UserService {
     @GET("users/info")
@@ -14,4 +11,7 @@ interface UserService {
     @Multipart
     @PATCH("users/update_avatar")
     suspend fun updateAvatar(@Part avatar: MultipartBody.Part): Response<UserInfo>
+
+    @POST("users/login")
+    suspend fun login(@Body user: LoginForm): Response<LoginResponse>
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/clemhaowen/dm_td2/tasklist/TaskListFragment.kt b/app/src/main/java/com/clemhaowen/dm_td2/tasklist/TaskListFragment.kt
index 08bf294..b5c50ef 100644
--- a/app/src/main/java/com/clemhaowen/dm_td2/tasklist/TaskListFragment.kt
+++ b/app/src/main/java/com/clemhaowen/dm_td2/tasklist/TaskListFragment.kt
@@ -83,6 +83,7 @@ class TaskListFragment : Fragment() {
 
             }
             startForResult.launch(intent)*/
+
             (recyclerView?.adapter as TaskListAdapter).notifyDataSetChanged()
         }
 
@@ -138,13 +139,16 @@ class TaskListFragment : Fragment() {
     override fun onResume() {
         super.onResume()
         lifecycleScope.launch {
-            val userInfo = Api.userService.getInfo().body()!!
-            val textView = view?.findViewById<TextView>(R.id.textViewInfo)
-            textView?.text = "${userInfo.firstName} ${userInfo.lastName}"
-            val profilPhoto = view?.findViewById<ImageView>(R.id.imageViewProfilPhoto)
-            profilPhoto?.load( userInfo?.avatar /*"https://goo.gl/gEgYUd"*/) {
-                crossfade(true)
-                transformations(CircleCropTransformation())
+            val userInfoResponse = Api.userService.getInfo()
+            if (userInfoResponse.isSuccessful) {
+                val userInfo = userInfoResponse.body()!!
+                val textView = view?.findViewById<TextView>(R.id.textViewInfo)
+                textView?.text = "${userInfo.firstName} ${userInfo.lastName}"
+                val profilPhoto = view?.findViewById<ImageView>(R.id.imageViewProfilPhoto)
+                profilPhoto?.load(userInfo.avatar /*"https://goo.gl/gEgYUd"*/) {
+                    crossfade(true)
+                    transformations(CircleCropTransformation())
+                }
             }
             // TODO comment this when refactoring is done.
             //tasksRepository.refresh()
diff --git a/app/src/main/java/com/clemhaowen/dm_td2/userInfo/AuthentificationFragment.kt b/app/src/main/java/com/clemhaowen/dm_td2/userInfo/AuthentificationFragment.kt
deleted file mode 100644
index c39de73..0000000
--- a/app/src/main/java/com/clemhaowen/dm_td2/userInfo/AuthentificationFragment.kt
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.clemhaowen.dm_td2.userInfo
-
-class AuthentificationFragment {
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/clemhaowen/dm_td2/userInfo/LoginFragment.kt b/app/src/main/java/com/clemhaowen/dm_td2/userInfo/LoginFragment.kt
deleted file mode 100644
index 12e3bf3..0000000
--- a/app/src/main/java/com/clemhaowen/dm_td2/userInfo/LoginFragment.kt
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.clemhaowen.dm_td2.userInfo
-
-class LoginFragment {
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/clemhaowen/dm_td2/userInfo/SignupFragment.kt b/app/src/main/java/com/clemhaowen/dm_td2/userInfo/SignupFragment.kt
deleted file mode 100644
index 66e6c8b..0000000
--- a/app/src/main/java/com/clemhaowen/dm_td2/userInfo/SignupFragment.kt
+++ /dev/null
@@ -1,4 +0,0 @@
-package com.clemhaowen.dm_td2.userInfo
-
-class SignupFragment {
-}
\ No newline at end of file
diff --git a/app/src/main/res/layout/authentification_fragment.xml b/app/src/main/res/layout/fragment_authentification.xml
similarity index 75%
rename from app/src/main/res/layout/authentification_fragment.xml
rename to app/src/main/res/layout/fragment_authentification.xml
index 0157645..e868484 100644
--- a/app/src/main/res/layout/authentification_fragment.xml
+++ b/app/src/main/res/layout/fragment_authentification.xml
@@ -1,14 +1,16 @@
 <?xml version="1.0" encoding="utf-8"?>
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
-    android:layout_height="match_parent">
+    android:layout_height="match_parent"
+    tools:context=".network.AuthentificationFragment">
 
     <Button
         android:id="@+id/login_button"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_weight="1"
-        android:text="Log in" />
+        android:text="Log In" />
 
     <Button
         android:id="@+id/signup_button"
@@ -16,4 +18,5 @@
         android:layout_height="wrap_content"
         android:layout_weight="1"
         android:text="Sign up" />
+
 </LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/login_fragment.xml b/app/src/main/res/layout/fragment_login.xml
similarity index 90%
rename from app/src/main/res/layout/login_fragment.xml
rename to app/src/main/res/layout/fragment_login.xml
index 73c447d..5c3eedd 100644
--- a/app/src/main/res/layout/login_fragment.xml
+++ b/app/src/main/res/layout/fragment_login.xml
@@ -1,7 +1,9 @@
 <?xml version="1.0" encoding="utf-8"?>
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
+    tools:context=".network.LoginFragment"
     android:orientation="vertical">
 
     <EditText
@@ -23,4 +25,5 @@
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:text="Log in" />
+
 </LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/signup_fragment.xml b/app/src/main/res/layout/fragment_signup.xml
similarity index 94%
rename from app/src/main/res/layout/signup_fragment.xml
rename to app/src/main/res/layout/fragment_signup.xml
index 564cd1e..167a62c 100644
--- a/app/src/main/res/layout/signup_fragment.xml
+++ b/app/src/main/res/layout/fragment_signup.xml
@@ -1,7 +1,9 @@
 <?xml version="1.0" encoding="utf-8"?>
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
+    tools:context=".network.SignupFragment"
     android:orientation="vertical">
 
     <EditText
@@ -46,4 +48,5 @@
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:text="Sign up" />
+
 </LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml
index c8384f7..08cd2b8 100644
--- a/app/src/main/res/navigation/nav_graph.xml
+++ b/app/src/main/res/navigation/nav_graph.xml
@@ -1,5 +1,31 @@
 <?xml version="1.0" encoding="utf-8"?>
 <navigation xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/nav_graph">
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/nav_graph"
+    app:startDestination="@id/authentificationFragment">
 
+
+    <fragment
+        android:id="@+id/authentificationFragment"
+        android:name="com.clemhaowen.dm_td2.network.AuthentificationFragment"
+        android:label="fragment_authentification"
+        tools:layout="@layout/fragment_authentification">
+        <action
+            android:id="@+id/action_authentificationFragment_to_loginFragment"
+            app:destination="@id/loginFragment" />
+        <action
+            android:id="@+id/action_authentificationFragment_to_signupFragment"
+            app:destination="@id/signupFragment" />
+    </fragment>
+    <fragment
+        android:id="@+id/loginFragment"
+        android:name="com.clemhaowen.dm_td2.network.LoginFragment"
+        android:label="fragment_login"
+        tools:layout="@layout/fragment_login" />
+    <fragment
+        android:id="@+id/signupFragment"
+        android:name="com.clemhaowen.dm_td2.network.SignupFragment"
+        android:label="fragment_signup"
+        tools:layout="@layout/fragment_signup" />
 </navigation>
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 748696f..683b228 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -9,4 +9,6 @@
 
     <string name="hello_first_fragment">Hello first fragment</string>
     <string name="hello_second_fragment">Hello second fragment. Arg: %1$s</string>
+    <!-- TODO: Remove or change this placeholder text -->
+    <string name="hello_blank_fragment">Hello blank fragment</string>
 </resources>
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 55e96ce..bf3a6f9 100644
--- a/build.gradle
+++ b/build.gradle
@@ -8,7 +8,8 @@ buildscript {
     dependencies {
         classpath "com.android.tools.build:gradle:4.1.0"
         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
-
+        def nav_version = "2.3.2"
+        classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
         // NOTE: Do not place your application dependencies here; they belong
         // in the individual module build.gradle files
     }
-- 
GitLab