🐛 Fixes poll editor
This commit is contained in:
@@ -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),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
Reference in New Issue
Block a user