🐛 Fixes poll editor

This commit is contained in:
2025-08-05 18:09:34 +08:00
parent d345c00e84
commit f3a8699389

View File

@@ -8,6 +8,7 @@ import 'package:island/pods/network.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/post/publishers_modal.dart'; import 'package:island/widgets/post/publishers_modal.dart';
import 'package:island/models/poll.dart'; import 'package:island/models/poll.dart';
import 'package:uuid/uuid.dart';
class PollEditorState { class PollEditorState {
String? id; // for editing String? id; // for editing
@@ -76,11 +77,17 @@ class PollEditor extends Notifier<PollEditorState> {
final nextOrder = state.questions.length; final nextOrder = state.questions.length;
final isOptionsType = _isOptionsType(type); final isOptionsType = _isOptionsType(type);
final q = SnPollQuestion( final q = SnPollQuestion(
id: 'local-$nextOrder', id: const Uuid().v4(),
type: type, type: type,
options: options:
isOptionsType isOptionsType
? [SnPollOption(id: 'opt-0', label: 'Option 1', order: 0)] ? [
SnPollOption(
id: const Uuid().v4(),
label: 'Option 1',
order: 0,
),
]
: null, : null,
title: '', title: '',
description: null, description: null,
@@ -155,7 +162,13 @@ class PollEditor extends Notifier<PollEditorState> {
isOptionsType isOptionsType
? (q.options?.isNotEmpty == true ? (q.options?.isNotEmpty == true
? q.options ? q.options
: [SnPollOption(id: 'opt-0', label: 'Option 1', order: 0)]) : [
SnPollOption(
id: const Uuid().v4(),
label: 'Option 1',
order: 0,
),
])
: null; : null;
_updateQuestion(index, q.copyWith(type: type, options: newOptions)); _updateQuestion(index, q.copyWith(type: type, options: newOptions));
} }
@@ -186,7 +199,7 @@ class PollEditor extends Notifier<PollEditorState> {
final nextOrder = opts.length; final nextOrder = opts.length;
opts.add( opts.add(
SnPollOption( SnPollOption(
id: 'opt-$nextOrder', id: const Uuid().v4(),
label: 'Option ${nextOrder + 1}', label: 'Option ${nextOrder + 1}',
order: nextOrder, order: nextOrder,
), ),
@@ -299,23 +312,9 @@ class PollEditorScreen extends ConsumerWidget {
}); });
// Submit helpers declared before build to avoid forward reference issues // Submit helpers declared before build to avoid forward reference issues
static String _mapTypeToServer(SnPollQuestionType t) {
switch (t) {
case SnPollQuestionType.singleChoice:
return 'SingleChoice';
case SnPollQuestionType.multipleChoice:
return 'MultipleChoice';
case SnPollQuestionType.freeText:
return 'FreeText';
case SnPollQuestionType.yesNo:
return 'YesNo';
case SnPollQuestionType.rating:
return 'Rating';
}
}
static Future<void> _submitPoll(BuildContext context, WidgetRef ref) async { static Future<void> _submitPoll(BuildContext context, WidgetRef ref) async {
final model = ref.read(pollEditorProvider); final model = ref.watch(pollEditorProvider);
final dio = ref.read(apiClientProvider); final dio = ref.read(apiClientProvider);
// Pick publisher (required) // Pick publisher (required)
@@ -349,7 +348,7 @@ class PollEditorScreen extends ConsumerWidget {
model.questions model.questions
.map( .map(
(q) => { (q) => {
'type': _mapTypeToServer(q.type), 'type': q.type.index,
'options': 'options':
q.options q.options
?.map( ?.map(
@@ -414,7 +413,7 @@ class PollEditorScreen extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final model = ref.watch(pollEditorProvider); final model = ref.watch(pollEditorProvider);
final notifier = ref.read(pollEditorProvider.notifier); final notifier = ref.watch(pollEditorProvider.notifier);
// initialize editing state if provided // initialize editing state if provided
if (initialPollId != null && model.id != initialPollId) { if (initialPollId != null && model.id != initialPollId) {
@@ -461,7 +460,7 @@ class PollEditorScreen extends ConsumerWidget {
return null; return null;
}, },
), ),
const SizedBox(height: 12), const Gap(12),
TextFormField( TextFormField(
initialValue: model.description ?? '', initialValue: model.description ?? '',
decoration: const InputDecoration( decoration: const InputDecoration(
@@ -477,12 +476,12 @@ class PollEditorScreen extends ConsumerWidget {
onTapOutside: onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(), (_) => FocusManager.instance.primaryFocus?.unfocus(),
), ),
const SizedBox(height: 12), const Gap(12),
_EndDatePicker( _EndDatePicker(
value: model.endedAt, value: model.endedAt,
onChanged: notifier.setEndedAt, onChanged: notifier.setEndedAt,
), ),
const SizedBox(height: 24), const Gap(24),
Row( Row(
children: [ children: [
Text( Text(
@@ -515,7 +514,7 @@ class PollEditorScreen extends ConsumerWidget {
), ),
], ],
), ),
const SizedBox(height: 8), const Gap(8),
if (model.questions.isEmpty) if (model.questions.isEmpty)
_EmptyState( _EmptyState(
title: 'No questions yet', title: 'No questions yet',
@@ -573,13 +572,18 @@ class PollEditorScreen extends ConsumerWidget {
); );
}, },
), ),
const SizedBox(height: 96), const Gap(96),
], ],
), ),
), ),
), ),
bottomNavigationBar: Padding( bottomNavigationBar: Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), padding: EdgeInsets.fromLTRB(
16,
8,
16,
16 + MediaQuery.of(context).padding.bottom,
),
child: Row( child: Row(
children: [ children: [
OutlinedButton.icon( OutlinedButton.icon(
@@ -726,7 +730,7 @@ class _EndDatePicker extends StatelessWidget {
), ),
), ),
], ],
const SizedBox(width: 8), const Gap(8),
TextButton( TextButton(
onPressed: () async { onPressed: () async {
final now = DateTime.now(); final now = DateTime.now();
@@ -839,7 +843,7 @@ class _QuestionEditor extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final notifier = ref.read(pollEditorProvider.notifier); final notifier = ref.watch(pollEditorProvider.notifier);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -865,7 +869,7 @@ class _QuestionEditor extends ConsumerWidget {
), ),
], ],
), ),
const SizedBox(height: 12), const Gap(12),
TextFormField( TextFormField(
initialValue: question.title, initialValue: question.title,
decoration: const InputDecoration( decoration: const InputDecoration(
@@ -885,7 +889,7 @@ class _QuestionEditor extends ConsumerWidget {
return null; return null;
}, },
), ),
const SizedBox(height: 12), const Gap(12),
TextFormField( TextFormField(
initialValue: question.description ?? '', initialValue: question.description ?? '',
decoration: const InputDecoration( decoration: const InputDecoration(
@@ -902,11 +906,11 @@ class _QuestionEditor extends ConsumerWidget {
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
), ),
if (question.options != null) ...[ if (question.options != null) ...[
const SizedBox(height: 16), const Gap(16),
Text('Options', style: Theme.of(context).textTheme.titleMedium), Text('Options', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8), const Gap(8),
_OptionsEditor(index: index, options: question.options!), _OptionsEditor(index: index, options: question.options!),
const SizedBox(height: 4), const Gap(4),
Align( Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: OutlinedButton.icon( child: OutlinedButton.icon(
@@ -920,7 +924,7 @@ class _QuestionEditor extends ConsumerWidget {
(question.type == SnPollQuestionType.freeText || (question.type == SnPollQuestionType.freeText ||
question.type == SnPollQuestionType.rating || question.type == SnPollQuestionType.rating ||
question.type == SnPollQuestionType.yesNo)) ...[ question.type == SnPollQuestionType.yesNo)) ...[
const SizedBox(height: 16), const Gap(16),
_TextAnswerPreview(long: false), _TextAnswerPreview(long: false),
], ],
], ],
@@ -952,7 +956,7 @@ class _QuestionTypePicker extends StatelessWidget {
child: Row( child: Row(
children: [ children: [
Icon(_iconForType(t)), Icon(_iconForType(t)),
const SizedBox(width: 8), const Gap(8),
Text(_labelForType(t)), Text(_labelForType(t)),
], ],
), ),
@@ -986,6 +990,7 @@ class _OptionsEditor extends ConsumerWidget {
children: [ children: [
Expanded( Expanded(
child: TextFormField( child: TextFormField(
key: ValueKey(options[i].id),
initialValue: options[i].label, initialValue: options[i].label,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Option label', labelText: 'Option label',
@@ -999,7 +1004,7 @@ class _OptionsEditor extends ConsumerWidget {
inputFormatters: [LengthLimitingTextInputFormatter(1024)], inputFormatters: [LengthLimitingTextInputFormatter(1024)],
), ),
), ),
const SizedBox(width: 8), const Gap(8),
SizedBox( SizedBox(
width: 40, width: 40,
child: IconButton( child: IconButton(
@@ -1077,13 +1082,13 @@ class _EmptyState extends StatelessWidget {
Icons.help_outline, Icons.help_outline,
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
), ),
const SizedBox(width: 12), const Gap(12),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(title, style: Theme.of(context).textTheme.titleMedium), Text(title, style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 4), const Gap(4),
Text(subtitle, style: Theme.of(context).textTheme.bodyMedium), Text(subtitle, style: Theme.of(context).textTheme.bodyMedium),
], ],
), ),