TokenResponse rework
This commit is contained in:
@@ -18,7 +18,7 @@
|
|||||||
</META>
|
</META>
|
||||||
|
|
||||||
<ROLE_DEFINITION>
|
<ROLE_DEFINITION>
|
||||||
<SPECIALIZATION>При исполнении этой роли, я, Gemini, действую как стратегический интерфейс между человеком-архитектором и автоматизированной системой разработки. Моя задача — вести итеративный диалог для уточнения целей, анализировать кодовую базу и, после получения одобрения, инициировать производственную цепочку через выбранный канал задач.</SPECIALIZATION>
|
<SPECIALIZATION>При исполнении этой роли, я действую как стратегический интерфейс между человеком-архитектором и автоматизированной системой разработки. Моя задача — вести итеративный диалог для уточнения целей, анализировать кодовую базу и, после получения одобрения, инициировать производственную цепочку через выбранный канал задач.</SPECIALIZATION>
|
||||||
<CORE_GOAL>Основная цель этой роли — трансформировать неструктурированный человеческий диалог в структурированный, машиночитаемый и полностью готовый к исполнению `Work Order` для роли 'Агента-Разработчика'.</CORE_GOAL>
|
<CORE_GOAL>Основная цель этой роли — трансформировать неструктурированный человеческий диалог в структурированный, машиночитаемый и полностью готовый к исполнению `Work Order` для роли 'Агента-Разработчика'.</CORE_GOAL>
|
||||||
</ROLE_DEFINITION>
|
</ROLE_DEFINITION>
|
||||||
|
|
||||||
|
|||||||
@@ -30,21 +30,6 @@
|
|||||||
</PHILOSOPHY_PRINCIPLE>
|
</PHILOSOPHY_PRINCIPLE>
|
||||||
</CORE_PHILOSOPHY>
|
</CORE_PHILOSOPHY>
|
||||||
|
|
||||||
<ISSUE_BODY_FORMAT name="Linting_Task_Specification">
|
|
||||||
<DESCRIPTION>Задачи для этой роли должны содержать XML-блок, определяющий режим работы.</DESCRIPTION>
|
|
||||||
<STRUCTURE>
|
|
||||||
<![CDATA[
|
|
||||||
<LINTING_TASK>
|
|
||||||
<MODE>full_project | recent_changes | single_file</MODE>
|
|
||||||
<TARGET>
|
|
||||||
<!-- Для recent_changes: commit range, e.g., HEAD~1..HEAD -->
|
|
||||||
<!-- Для single_file: path/to/file.kt -->
|
|
||||||
</TARGET>
|
|
||||||
</LINTING_TASK>
|
|
||||||
]]>
|
|
||||||
</STRUCTURE>
|
|
||||||
</ISSUE_BODY_FORMAT>
|
|
||||||
|
|
||||||
<MASTER_WORKFLOW name="Lint_And_Create_Pull_Request_Cycle">
|
<MASTER_WORKFLOW name="Lint_And_Create_Pull_Request_Cycle">
|
||||||
<WORKFLOW_STEP id="1" name="Find_And_Acknowledge_Task">
|
<WORKFLOW_STEP id="1" name="Find_And_Acknowledge_Task">
|
||||||
<LET name="WorkOrder" value="CALL MyTaskChannel.FindNextTask(RoleName='agent-linter', TaskType='type::linting')"/>
|
<LET name="WorkOrder" value="CALL MyTaskChannel.FindNextTask(RoleName='agent-linter', TaskType='type::linting')"/>
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ data class TokenResponseDto(
|
|||||||
*/
|
*/
|
||||||
fun TokenResponseDto.toDomain(): TokenResponse {
|
fun TokenResponseDto.toDomain(): TokenResponse {
|
||||||
return TokenResponse(
|
return TokenResponse(
|
||||||
token = this.token
|
token = this.token,
|
||||||
|
attachmentToken = this.attachmentToken,
|
||||||
|
expiresAt = this.expiresAt
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// [END_ENTITY: Function('toDomain')]
|
// [END_ENTITY: Function('toDomain')]
|
||||||
|
|||||||
@@ -4,9 +4,6 @@
|
|||||||
|
|
||||||
package com.homebox.lens.data.api.model
|
package com.homebox.lens.data.api.model
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
|
||||||
import com.squareup.moshi.JsonClass
|
|
||||||
|
|
||||||
// [IMPORTS]
|
// [IMPORTS]
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import com.homebox.lens.domain.model.LocationOut as DomainLocationOut
|
|||||||
import com.homebox.lens.domain.model.LocationOutCount as DomainLocationOutCount
|
import com.homebox.lens.domain.model.LocationOutCount as DomainLocationOutCount
|
||||||
import com.homebox.lens.domain.model.MaintenanceEntry as DomainMaintenanceEntry
|
import com.homebox.lens.domain.model.MaintenanceEntry as DomainMaintenanceEntry
|
||||||
import com.homebox.lens.domain.model.PaginationResult as DomainPaginationResult
|
import com.homebox.lens.domain.model.PaginationResult as DomainPaginationResult
|
||||||
|
import com.homebox.lens.domain.model.TokenResponse as DomainTokenResponse
|
||||||
// [END_IMPORTS]
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [ENTITY: Function('ItemOutDto.toDomain')]
|
// [ENTITY: Function('ItemOutDto.toDomain')]
|
||||||
@@ -265,4 +266,5 @@ fun LabelSummaryDto.toDomain(): DomainLabelSummary {
|
|||||||
}
|
}
|
||||||
// [END_ENTITY: Function('LabelSummaryDto.toDomain')]
|
// [END_ENTITY: Function('LabelSummaryDto.toDomain')]
|
||||||
|
|
||||||
|
|
||||||
// [END_FILE_DtoToDomain.kt]
|
// [END_FILE_DtoToDomain.kt]
|
||||||
@@ -8,8 +8,9 @@ package com.homebox.lens.data.repository
|
|||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import com.homebox.lens.data.api.HomeboxApiService
|
import com.homebox.lens.data.api.HomeboxApiService
|
||||||
import com.homebox.lens.data.api.dto.LoginFormDto
|
import com.homebox.lens.data.api.dto.LoginFormDto
|
||||||
import com.homebox.lens.data.api.mapper.toDomain
|
import com.homebox.lens.data.api.dto.toDomain
|
||||||
import com.homebox.lens.domain.model.Credentials
|
import com.homebox.lens.domain.model.Credentials
|
||||||
|
import com.homebox.lens.domain.model.Result
|
||||||
import com.homebox.lens.domain.model.TokenResponse
|
import com.homebox.lens.domain.model.TokenResponse
|
||||||
import com.homebox.lens.domain.repository.AuthRepository
|
import com.homebox.lens.domain.repository.AuthRepository
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -56,7 +57,7 @@ class AuthRepositoryImpl @Inject constructor(
|
|||||||
require(credentials.serverUrl.isNotBlank()) { "Server URL cannot be blank." }
|
require(credentials.serverUrl.isNotBlank()) { "Server URL cannot be blank." }
|
||||||
|
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
runCatching {
|
try {
|
||||||
Timber.d("[DEBUG][ACTION][creating_retrofit_client] Creating temporary Retrofit client for URL: ${credentials.serverUrl}")
|
Timber.d("[DEBUG][ACTION][creating_retrofit_client] Creating temporary Retrofit client for URL: ${credentials.serverUrl}")
|
||||||
val tempApiService = Retrofit.Builder()
|
val tempApiService = Retrofit.Builder()
|
||||||
.baseUrl(credentials.serverUrl)
|
.baseUrl(credentials.serverUrl)
|
||||||
@@ -70,7 +71,10 @@ class AuthRepositoryImpl @Inject constructor(
|
|||||||
val tokenResponseDto = tempApiService.login(loginForm)
|
val tokenResponseDto = tempApiService.login(loginForm)
|
||||||
|
|
||||||
Timber.d("[DEBUG][ACTION][mapping_to_domain] Mapping token response to domain model.")
|
Timber.d("[DEBUG][ACTION][mapping_to_domain] Mapping token response to domain model.")
|
||||||
tokenResponseDto.toDomain()
|
Result.Success(tokenResponseDto.toDomain())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "[ERROR][FAILURE][login_failed] Login failed.")
|
||||||
|
Result.Error(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,15 @@ package com.homebox.lens.domain.model
|
|||||||
/**
|
/**
|
||||||
* @summary Data model representing a response from the server with an authentication token.
|
* @summary Data model representing a response from the server with an authentication token.
|
||||||
* @param token A string containing a JWT or other access token.
|
* @param token A string containing a JWT or other access token.
|
||||||
|
* @param attachmentToken A token for accessing attachments.
|
||||||
|
* @param expiresAt The expiration date of the token.
|
||||||
* @invariant `token` must not be blank.
|
* @invariant `token` must not be blank.
|
||||||
*/
|
*/
|
||||||
data class TokenResponse(val token: String) {
|
data class TokenResponse(
|
||||||
|
val token: String,
|
||||||
|
val attachmentToken: String,
|
||||||
|
val expiresAt: String
|
||||||
|
) {
|
||||||
init {
|
init {
|
||||||
require(token.isNotBlank()) { "Token cannot be blank." }
|
require(token.isNotBlank()) { "Token cannot be blank." }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,17 +31,19 @@ class LoginUseCase @Inject constructor(
|
|||||||
"Server URL and username must not be blank."
|
"Server URL and username must not be blank."
|
||||||
}
|
}
|
||||||
|
|
||||||
val loginResult: com.homebox.lens.domain.model.Result<TokenResponse> = authRepository.login(credentials)
|
return try {
|
||||||
|
when (val loginResult = authRepository.login(credentials)) {
|
||||||
return loginResult.fold(
|
is com.homebox.lens.domain.model.Result.Success -> {
|
||||||
onSuccess = {
|
authRepository.saveToken(loginResult.data.token)
|
||||||
authRepository.saveToken(it.token)
|
Result.success(Unit)
|
||||||
com.homebox.lens.domain.model.Result.Success(Unit)
|
}
|
||||||
},
|
is com.homebox.lens.domain.model.Result.Error -> {
|
||||||
onFailure = {
|
Result.failure(loginResult.exception)
|
||||||
com.homebox.lens.domain.model.Result.Error(it as Exception)
|
}
|
||||||
}
|
}
|
||||||
)
|
} catch (e: Exception) {
|
||||||
|
Result.failure(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// [END_ENTITY: Function('invoke')]
|
// [END_ENTITY: Function('invoke')]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
<![CDATA[
|
|
||||||
<WORK_ORDER>
|
|
||||||
<METADATA>
|
|
||||||
<ID>enrichment-task-001</ID>
|
|
||||||
<TITLE>Perform initial semantic enrichment</TITLE>
|
|
||||||
<ROLE_TARGET>agent-enrichment</ROLE_TARGET>
|
|
||||||
<TYPE>type::enrichment</TYPE>
|
|
||||||
<STATUS>status::pending</STATUS>
|
|
||||||
</METADATA>
|
|
||||||
<TASK_BODY>
|
|
||||||
<ENRICHMENT_TASK>
|
|
||||||
<SCOPE>full_project</SCOPE>
|
|
||||||
<TARGET></TARGET>
|
|
||||||
</ENRICHMENT_TASK>
|
|
||||||
</TASK_BODY>
|
|
||||||
</WORK_ORDER>
|
|
||||||
]]>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<![CDATA[
|
|
||||||
<ENRICHMENT_TASK>
|
|
||||||
<SCOPE>directory</SCOPE>
|
|
||||||
<TARGET>app/src/main/java/com/homebox/lens/ui</TARGET>
|
|
||||||
</ENRICHMENT_TASK>
|
|
||||||
]]>
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<WORK_ORDER>
|
|
||||||
<META>
|
|
||||||
<ID>WO-ITEMEDIT-FIX</ID>
|
|
||||||
<TITLE>[ARCHITECT -> DEV] Исправление выбора локации и меток на экране ItemEdit</TITLE>
|
|
||||||
<DESCRIPTION>В текущей реализации на экране редактирования/создания элемента (ItemEditScreen) поля "Location" и "Labels" неактивны. Необходимо реализовать функционал выбора значения для этих полей из списка доступных.</DESCRIPTION>
|
|
||||||
<CREATED_BY>architect-agent</CREATED_BY>
|
|
||||||
<ASSIGNED_TO>developer-agent</ASSIGNED_TO>
|
|
||||||
<STATUS>pending</STATUS>
|
|
||||||
</META>
|
|
||||||
|
|
||||||
<TASK_BREAKDOWN>
|
|
||||||
<STEP n="1" file="app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditViewModel.kt">
|
|
||||||
<ACTION>Загрузка списков локаций и меток.</ACTION>
|
|
||||||
<DETAILS>
|
|
||||||
1. Внедрите `GetAllLocationsUseCase` и `GetAllLabelsUseCase` в `ItemEditViewModel`.
|
|
||||||
2. Обновите `ItemEditUiState`, добавив два новых поля: `val allLocations: List<Location> = emptyList()` и `val allLabels: List<Label> = emptyList()`.
|
|
||||||
3. В функции `loadItem`, после загрузки основной информации о товаре, вызовите `getAllLocationsUseCase` и `getAllLabelsUseCase` и обновите `uiState` полученными списками.
|
|
||||||
4. Добавьте публичные методы `updateLocation(location: Location)` и `updateLabels(labels: List<Label>)` для обновления `item` в `uiState`.
|
|
||||||
</DETAILS>
|
|
||||||
</STEP>
|
|
||||||
|
|
||||||
<STEP n="2" file="app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt">
|
|
||||||
<ACTION>Реализация UI для выбора локации.</ACTION>
|
|
||||||
<DETAILS>
|
|
||||||
1. Замените `OutlinedTextField` для локации на `ExposedDropdownMenuBox`.
|
|
||||||
2. В качестве `dropdownMenu` используйте `DropdownMenuItem` для каждого элемента из `uiState.allLocations`.
|
|
||||||
3. При выборе элемента из списка вызывайте `viewModel.updateLocation(selectedLocation)`.
|
|
||||||
4. В `ExposedDropdownMenuBox` должно отображаться `item.location?.name`.
|
|
||||||
</DETAILS>
|
|
||||||
</STEP>
|
|
||||||
|
|
||||||
<STEP n="3" file="app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt">
|
|
||||||
<ACTION>Реализация UI для выбора меток (множественный выбор).</ACTION>
|
|
||||||
<DETAILS>
|
|
||||||
1. Поле для меток `Labels` должно оставаться `OutlinedTextField` (read-only), но `onClick` по нему должен открывать диалоговое окно (`AlertDialog`).
|
|
||||||
2. В `AlertDialog` отобразите список всех меток (`uiState.allLabels`) с `Checkbox`'ами.
|
|
||||||
3. Состояние `Checkbox`'ов должно соответствовать списку `item.labels`.
|
|
||||||
4. При нажатии на "OK" в диалоге, вызывайте `viewModel.updateLabels(selectedLabels)`.
|
|
||||||
</DETAILS>
|
|
||||||
</STEP>
|
|
||||||
</TASK_BREAKDOWN>
|
|
||||||
|
|
||||||
<ACCEPTANCE_CRITERIA>
|
|
||||||
<CRITERION>При нажатии на поле "Location" открывается выпадающий список со всеми локациями.</CRITERION>
|
|
||||||
<CRITERION>Выбранная локация отображается в поле и сохраняется вместе с элементом.</CRITERION>
|
|
||||||
<CRITERION>При нажатии на поле "Labels" открывается диалоговое окно со списком всех меток и чекбоксами.</CRITERION>
|
|
||||||
<CRITERION>Выбранные метки отображаются в поле и сохраняются вместе с элементом.</CRITERION>
|
|
||||||
</ACCEPTANCE_CRITERIA>
|
|
||||||
|
|
||||||
</WORK_ORDER>
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<WORK_ORDER>
|
|
||||||
<META>
|
|
||||||
<ID>WO-LOGIN-REFACTOR</ID>
|
|
||||||
<TITLE>[ARCHITECT -> DEV] Рефакторинг экрана входа и логики первого запуска</TITLE>
|
|
||||||
<DESCRIPTION>Цель этой задачи - изменить логику запуска приложения. Экран входа (SetupScreen) должен появляться только при первом запуске, когда учетные данные еще не сохранены. В последующие запуски пользователь должен сразу попадать на главный экран (Dashboard). Также необходимо улучшить визуальное оформление экрана входа.</DESCRIPTION>
|
|
||||||
<CREATED_BY>architect-agent</CREATED_BY>
|
|
||||||
<ASSIGNED_TO>developer-agent</ASSIGNED_TO>
|
|
||||||
<STATUS>pending</STATUS>
|
|
||||||
</META>
|
|
||||||
|
|
||||||
<TASK_BREAKDOWN>
|
|
||||||
<STEP n="1" file="app/src/main/java/com/homebox/lens/ui/screen/setup/SetupViewModel.kt">
|
|
||||||
<ACTION>Добавить public-метод для синхронной проверки наличия учетных данных.</ACTION>
|
|
||||||
<DETAILS>
|
|
||||||
Добавьте в класс `SetupViewModel` новый метод `fun areCredentialsSaved(): Boolean`.
|
|
||||||
Этот метод должен синхронно проверять, сохранены ли учетные данные в `CredentialsRepository`.
|
|
||||||
Текущая реализация `getCredentials()` асинхронна, что не подходит для быстрой проверки в `NavGraph`.
|
|
||||||
Вам может потребоваться изменить `CredentialsRepository` для поддержки синхронной проверки (например, используя `SharedPreferences` напрямую).
|
|
||||||
</DETAILS>
|
|
||||||
</STEP>
|
|
||||||
|
|
||||||
<STEP n="2" file="app/src/main/java/com/homebox/lens/ui/screen/splash/SplashScreen.kt">
|
|
||||||
<ACTION>Создать новый `SplashScreen`.</ACTION>
|
|
||||||
<DETAILS>
|
|
||||||
Создайте новый Composable-экран `SplashScreen.kt`.
|
|
||||||
Этот экран будет новой точкой входа в `NavGraph`.
|
|
||||||
Он будет использовать `SetupViewModel` для вызова `areCredentialsSaved()` и, в зависимости от результата, немедленно навигироваться либо на `Screen.Setup`, либо на `Screen.Dashboard`.
|
|
||||||
Пока идет проверка, на экране должен отображаться `CircularProgressIndicator`.
|
|
||||||
</DETAILS>
|
|
||||||
</STEP>
|
|
||||||
|
|
||||||
<STEP n="3" file="app/src/main/java/com/homebox/lens/navigation/NavGraph.kt">
|
|
||||||
<ACTION>Обновить `NavGraph` для использования `SplashScreen`.</ACTION>
|
|
||||||
<DETAILS>
|
|
||||||
Измените `startDestination` в `NavHost` на `Screen.Splash.route`.
|
|
||||||
Добавьте `composable` для `SplashScreen`.
|
|
||||||
В `SplashScreen` вызовите `navController.navigate` с очисткой бэкстека (`popUpTo(Screen.Splash.route) { inclusive = true }`), чтобы пользователь не мог вернуться на сплэш-экран.
|
|
||||||
</DETAILS>
|
|
||||||
</STEP>
|
|
||||||
|
|
||||||
<STEP n="4" file="app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt">
|
|
||||||
<ACTION>Улучшить UI экрана `SetupScreen`.</ACTION>
|
|
||||||
<DETAILS>
|
|
||||||
Текущий UI слишком прост. Добавьте заголовок, иконку приложения, и более приятное расположение элементов.
|
|
||||||
Используйте `Card` для группировки полей ввода. Добавьте `Spacer` для лучшего отступа.
|
|
||||||
Кнопку "Connect" сделайте более заметной.
|
|
||||||
</DETAILS>
|
|
||||||
</STEP>
|
|
||||||
|
|
||||||
</TASK_BREAKDOWN>
|
|
||||||
|
|
||||||
<ACCEPTANCE_CRITERIA>
|
|
||||||
<CRITERION>При первом запуске приложения открывается `SetupScreen`.</CRITERION>
|
|
||||||
<CRITERION>После успешного ввода данных и входа, при последующих перезапусках приложения открывается `DashboardScreen`, минуя `SetupScreen`.</CRITERION>
|
|
||||||
<CRITERION>`SetupScreen` имеет улучшенный и более привлекательный дизайн.</CRITERION>
|
|
||||||
</ACCEPTANCE_CRITERIA>
|
|
||||||
|
|
||||||
</WORK_ORDER>
|
|
||||||
Reference in New Issue
Block a user