qa roles
This commit is contained in:
@@ -20,6 +20,7 @@ import com.homebox.lens.ui.screen.inventorylist.InventoryListScreen
|
||||
import com.homebox.lens.ui.screen.itemdetails.ItemDetailsScreen
|
||||
import com.homebox.lens.ui.screen.itemedit.ItemEditScreen
|
||||
import com.homebox.lens.ui.screen.labelslist.LabelsListScreen
|
||||
import com.homebox.lens.ui.screen.labeledit.LabelEditScreen
|
||||
import com.homebox.lens.ui.screen.locationedit.LocationEditScreen
|
||||
import com.homebox.lens.ui.screen.locationslist.LocationsListScreen
|
||||
import com.homebox.lens.ui.screen.search.SearchScreen
|
||||
@@ -110,6 +111,23 @@ fun NavGraph(
|
||||
locationId = locationId
|
||||
)
|
||||
}
|
||||
composable(route = Screen.LocationEdit.route) { backStackEntry ->
|
||||
val locationId = backStackEntry.arguments?.getString("locationId")
|
||||
LocationEditScreen(
|
||||
locationId = locationId
|
||||
)
|
||||
}
|
||||
composable(
|
||||
route = Screen.LabelEdit.route,
|
||||
arguments = listOf(navArgument("labelId") { nullable = true })
|
||||
) { backStackEntry ->
|
||||
val labelId = backStackEntry.arguments?.getString("labelId")
|
||||
LabelEditScreen(
|
||||
labelId = labelId,
|
||||
onBack = { navController.popBackStack() },
|
||||
onLabelSaved = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
composable(route = Screen.Search.route) {
|
||||
SearchScreen(
|
||||
currentRoute = currentRoute,
|
||||
|
||||
@@ -77,6 +77,21 @@ sealed class Screen(val route: String) {
|
||||
data object LabelsList : Screen("labels_list_screen")
|
||||
// [END_ENTITY: Object('LabelsList')]
|
||||
|
||||
// [ENTITY: Object('LabelEdit')]
|
||||
data object LabelEdit : Screen("label_edit_screen?labelId={labelId}") {
|
||||
// [ENTITY: Function('createRoute')]
|
||||
/**
|
||||
* @summary Создает маршрут для экрана редактирования метки с указанным ID.
|
||||
* @param labelId ID метки для редактирования. Null, если создается новая метка.
|
||||
* @return Строку полного маршрута.
|
||||
*/
|
||||
fun createRoute(labelId: String? = null): String {
|
||||
return labelId?.let { "label_edit_screen?labelId=$it" } ?: "label_edit_screen"
|
||||
}
|
||||
// [END_ENTITY: Function('createRoute')]
|
||||
}
|
||||
// [END_ENTITY: Object('LabelEdit')]
|
||||
|
||||
// [ENTITY: Object('LocationsList')]
|
||||
data object LocationsList : Screen("locations_list_screen")
|
||||
// [END_ENTITY: Object('LocationsList')]
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.components
|
||||
// [FILE] ColorPicker.kt
|
||||
// [SEMANTICS] ui, component, color_selection
|
||||
|
||||
package com.homebox.lens.ui.components
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.homebox.lens.R
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: Function('ColorPicker')]
|
||||
/**
|
||||
* @summary Компонент для выбора цвета.
|
||||
* @param selectedColor Текущий выбранный цвет в формате HEX строки (например, "#FFFFFF").
|
||||
* @param onColorSelected Лямбда-функция, вызываемая при выборе нового цвета.
|
||||
* @param modifier Модификатор для настройки внешнего вида.
|
||||
*/
|
||||
@Composable
|
||||
fun ColorPicker(
|
||||
selectedColor: String,
|
||||
onColorSelected: (String) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
Text(text = stringResource(R.string.label_color), style = MaterialTheme.typography.bodyLarge)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.background(Color(android.graphics.Color.parseColor(selectedColor)), CircleShape)
|
||||
.border(2.dp, MaterialTheme.colorScheme.onSurface, CircleShape)
|
||||
.clickable { /* TODO: Implement a more advanced color selection dialog */ }
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
OutlinedTextField(
|
||||
value = selectedColor,
|
||||
onValueChange = { newValue ->
|
||||
// Basic validation for hex color
|
||||
if (newValue.matches(Regex("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$"))) {
|
||||
onColorSelected(newValue)
|
||||
} else if (newValue.isEmpty() || newValue == "#") {
|
||||
onColorSelected("#FFFFFF") // Default to white if input is cleared
|
||||
}
|
||||
},
|
||||
label = { Text(stringResource(R.string.label_hex_color)) },
|
||||
singleLine = true,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('ColorPicker')]
|
||||
// [END_FILE_ColorPicker.kt]
|
||||
@@ -0,0 +1,35 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.components
|
||||
// [FILE] LoadingOverlay.kt
|
||||
// [SEMANTICS] ui, component, loading
|
||||
|
||||
package com.homebox.lens.ui.components
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: Function('LoadingOverlay')]
|
||||
/**
|
||||
* @summary Полноэкранный оверлей с индикатором загрузки.
|
||||
*/
|
||||
@Composable
|
||||
fun LoadingOverlay() {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.surface.copy(alpha = 0.6f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('LoadingOverlay')]
|
||||
// [END_FILE_LoadingOverlay.kt]
|
||||
@@ -0,0 +1,113 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.labeledit
|
||||
// [FILE] LabelEditScreen.kt
|
||||
// [SEMANTICS] ui, screen, label, edit
|
||||
|
||||
package com.homebox.lens.ui.screen.labeledit
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.homebox.lens.R
|
||||
import com.homebox.lens.ui.components.ColorPicker
|
||||
import com.homebox.lens.ui.components.LoadingOverlay
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: Function('LabelEditScreen')]
|
||||
// [RELATION: Function('LabelEditScreen')] -> [DEPENDS_ON] -> [ViewModel('LabelEditViewModel')]
|
||||
/**
|
||||
* @summary Composable-функция для экрана "Редактирование метки".
|
||||
* @param labelId ID метки для редактирования или null для создания новой.
|
||||
* @param onBack Навигация назад.
|
||||
* @param onLabelSaved Действие после сохранения метки.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LabelEditScreen(
|
||||
labelId: String?,
|
||||
onBack: () -> Unit,
|
||||
onLabelSaved: () -> Unit,
|
||||
viewModel: LabelEditViewModel = hiltViewModel()
|
||||
) {
|
||||
val uiState = viewModel.uiState
|
||||
val snackbarHostState = SnackbarHostState()
|
||||
|
||||
LaunchedEffect(uiState.isSaved) {
|
||||
if (uiState.isSaved) {
|
||||
onLabelSaved()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(uiState.error) {
|
||||
uiState.error?.let {
|
||||
snackbarHostState.showSnackbar(
|
||||
message = it,
|
||||
actionLabel = "Dismiss",
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = if (labelId == null) {
|
||||
stringResource(id = R.string.label_edit_title_create)
|
||||
} else {
|
||||
stringResource(id = R.string.label_edit_title_edit)
|
||||
}
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back))
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = viewModel::saveLabel) {
|
||||
Icon(Icons.Default.Check, contentDescription = stringResource(R.string.save))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = uiState.name,
|
||||
onValueChange = viewModel::onNameChange,
|
||||
label = { Text(stringResource(R.string.label_name)) },
|
||||
isError = uiState.nameError != null,
|
||||
supportingText = { uiState.nameError?.let { Text(it) } },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
ColorPicker(
|
||||
selectedColor = uiState.color,
|
||||
onColorSelected = viewModel::onColorChange,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
|
||||
if (uiState.isLoading) {
|
||||
LoadingOverlay()
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('LabelEditScreen')]
|
||||
// [END_FILE_LabelEditScreen.kt]
|
||||
@@ -0,0 +1,115 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.labeledit
|
||||
// [FILE] LabelEditViewModel.kt
|
||||
// [SEMANTICS] ui, viewmodel, label_management
|
||||
|
||||
package com.homebox.lens.ui.screen.labeledit
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.homebox.lens.domain.model.LabelCreate
|
||||
import com.homebox.lens.domain.model.LabelOut
|
||||
import com.homebox.lens.domain.model.LabelUpdate
|
||||
import com.homebox.lens.domain.usecase.CreateLabelUseCase
|
||||
import com.homebox.lens.domain.usecase.GetLabelDetailsUseCase
|
||||
import com.homebox.lens.domain.usecase.UpdateLabelUseCase
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: ViewModel('LabelEditViewModel')]
|
||||
// [RELATION: ViewModel('LabelEditViewModel')] -> [DEPENDS_ON] -> [UseCase('GetLabelDetailsUseCase')]
|
||||
// [RELATION: ViewModel('LabelEditViewModel')] -> [DEPENDS_ON] -> [UseCase('CreateLabelUseCase')]
|
||||
// [RELATION: ViewModel('LabelEditViewModel')] -> [DEPENDS_ON] -> [UseCase('UpdateLabelUseCase')]
|
||||
// [RELATION: ViewModel('LabelEditViewModel')] -> [EMITS_STATE] -> [DataClass('LabelEditUiState')]
|
||||
@HiltViewModel
|
||||
class LabelEditViewModel @Inject constructor(
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
private val getLabelDetailsUseCase: GetLabelDetailsUseCase,
|
||||
private val createLabelUseCase: CreateLabelUseCase,
|
||||
private val updateLabelUseCase: UpdateLabelUseCase
|
||||
) : ViewModel() {
|
||||
|
||||
var uiState by mutableStateOf(LabelEditUiState())
|
||||
private set
|
||||
|
||||
private val labelId: String? = savedStateHandle["labelId"]
|
||||
|
||||
init {
|
||||
if (labelId != null) {
|
||||
loadLabelDetails(labelId)
|
||||
}
|
||||
}
|
||||
|
||||
fun onNameChange(newName: String) {
|
||||
uiState = uiState.copy(name = newName, nameError = null)
|
||||
}
|
||||
|
||||
fun onColorChange(newColor: String) {
|
||||
uiState = uiState.copy(color = newColor)
|
||||
}
|
||||
|
||||
fun saveLabel() {
|
||||
viewModelScope.launch {
|
||||
if (uiState.name.isBlank()) {
|
||||
uiState = uiState.copy(nameError = "Label name cannot be empty.")
|
||||
return@launch
|
||||
}
|
||||
|
||||
uiState = uiState.copy(isLoading = true, error = null)
|
||||
try {
|
||||
if (labelId == null) {
|
||||
// Create new label
|
||||
val newLabel = LabelCreate(name = uiState.name, color = uiState.color)
|
||||
createLabelUseCase(newLabel)
|
||||
} else {
|
||||
// Update existing label
|
||||
val updatedLabel = LabelUpdate(name = uiState.name, color = uiState.color)
|
||||
updateLabelUseCase(labelId, updatedLabel)
|
||||
}
|
||||
uiState = uiState.copy(isSaved = true)
|
||||
} catch (e: Exception) {
|
||||
uiState = uiState.copy(error = e.message, isLoading = false)
|
||||
} finally {
|
||||
uiState = uiState.copy(isLoading = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadLabelDetails(id: String) {
|
||||
viewModelScope.launch {
|
||||
uiState = uiState.copy(isLoading = true, error = null)
|
||||
try {
|
||||
val label = getLabelDetailsUseCase(id)
|
||||
uiState = uiState.copy(
|
||||
name = label.name,
|
||||
color = label.color,
|
||||
isLoading = false
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
uiState = uiState.copy(error = e.message, isLoading = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// [ENTITY: DataClass('LabelEditUiState')]
|
||||
/**
|
||||
* @summary Состояние UI для экрана редактирования метки.
|
||||
*/
|
||||
data class LabelEditUiState(
|
||||
val name: String = "",
|
||||
val color: String = "#FFFFFF", // Default color
|
||||
val nameError: String? = null,
|
||||
val isLoading: Boolean = false,
|
||||
val error: String? = null,
|
||||
val isSaved: Boolean = false,
|
||||
val originalLabel: LabelOut? = null // To hold original label details if editing
|
||||
)
|
||||
// [END_ENTITY: DataClass('LabelEditUiState')]
|
||||
// [END_FILE_LabelEditViewModel.kt]
|
||||
@@ -82,8 +82,8 @@ fun LabelsListScreen(
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(onClick = {
|
||||
Timber.i("[INFO][ACTION][show_create_dialog] FAB clicked: Initiate create new label flow.")
|
||||
viewModel.onShowCreateDialog()
|
||||
Timber.i("[INFO][ACTION][navigate_to_label_edit] FAB clicked: Navigate to create new label screen.")
|
||||
navController.navigate(Screen.LabelEdit.createRoute())
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
@@ -93,16 +93,6 @@ fun LabelsListScreen(
|
||||
}
|
||||
) { paddingValues ->
|
||||
val currentState = uiState
|
||||
if (currentState is LabelsListUiState.Success && currentState.isShowingCreateDialog) {
|
||||
CreateLabelDialog(
|
||||
onConfirm = { labelName ->
|
||||
viewModel.createLabel(labelName)
|
||||
},
|
||||
onDismiss = {
|
||||
viewModel.onDismissCreateDialog()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@@ -119,14 +109,13 @@ fun LabelsListScreen(
|
||||
}
|
||||
is LabelsListUiState.Success -> {
|
||||
if (currentState.labels.isEmpty()) {
|
||||
Text(text = stringResource(id = R.string.labels_list_empty))
|
||||
Text(text = stringResource(id = R.string.no_labels_found))
|
||||
} else {
|
||||
LabelsList(
|
||||
labels = currentState.labels,
|
||||
onLabelClick = { label ->
|
||||
Timber.i("[INFO][ACTION][navigate_to_inventory] Label clicked: ${label.id}. Navigating to inventory list.")
|
||||
val route = Screen.InventoryList.withFilter("label", label.id)
|
||||
navController.navigate(route)
|
||||
Timber.i("[INFO][ACTION][navigate_to_label_edit] Label clicked: ${label.id}. Navigating to label edit screen.")
|
||||
navController.navigate(Screen.LabelEdit.createRoute(label.id))
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -191,46 +180,4 @@ private fun LabelListItem(
|
||||
}
|
||||
// [END_ENTITY: Function('LabelListItem')]
|
||||
|
||||
// [ENTITY: Function('CreateLabelDialog')]
|
||||
/**
|
||||
* @summary Диалоговое окно для создания новой метки.
|
||||
* @param onConfirm Лямбда-функция, вызываемая при подтверждении создания с именем метки.
|
||||
* @param onDismiss Лямбда-функция, вызываемая при закрытии диалога.
|
||||
*/
|
||||
@Composable
|
||||
private fun CreateLabelDialog(
|
||||
onConfirm: (String) -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
var text by remember { mutableStateOf("") }
|
||||
val isConfirmEnabled = text.isNotBlank()
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(text = stringResource(R.string.dialog_title_create_label)) },
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = text,
|
||||
onValueChange = { text = it },
|
||||
label = { Text(stringResource(R.string.dialog_field_label_name)) },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = { onConfirm(text) },
|
||||
enabled = isConfirmEnabled
|
||||
) {
|
||||
Text(stringResource(R.string.dialog_button_create))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(R.string.dialog_button_cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('CreateLabelDialog')]
|
||||
// [END_FILE_LabelsListScreen.kt]
|
||||
@@ -99,4 +99,17 @@
|
||||
<string name="dialog_button_create">Создать</string>
|
||||
<string name="dialog_button_cancel">Отмена</string>
|
||||
|
||||
<!-- Label Edit Screen -->
|
||||
<string name="label_edit_title_create">Создать метку</string>
|
||||
<string name="label_edit_title_edit">Редактировать метку</string>
|
||||
<string name="label_name_edit">Название метки</string>
|
||||
|
||||
<!-- Common Actions -->
|
||||
<string name="back">Назад</string>
|
||||
<string name="save">Сохранить</string>
|
||||
<!-- Common Actions -->
|
||||
|
||||
<!-- Color Picker -->
|
||||
<string name="label_color">Цвет</string>
|
||||
<string name="label_hex_color">HEX-код цвета</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user