Skip to content

Enhance content management #50

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 48 commits into from
Aug 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
351a651
refactor(content_management): rename deletion events to archiving
fulleni Aug 2, 2025
c3c0fb6
feat(content_management): implement archive functionality for headlin…
fulleni Aug 2, 2025
1013dc5
feat(content_management): replace delete functionality with archive
fulleni Aug 2, 2025
e76dc71
refactor(content_management): replace delete functionality with archive
fulleni Aug 2, 2025
632be8f
refactor(content_management): replace delete action with archive
fulleni Aug 2, 2025
7c1dd74
fix(content_management): allow text overflow for headline title
fulleni Aug 2, 2025
f8dd350
refactor(content_management): improve topics page UI and code consist…
fulleni Aug 2, 2025
08af2ed
fix(content_management): allow source name to overflow with an ellipsis
fulleni Aug 2, 2025
2853886
fix(content_management): improve headline pagination and UI
fulleni Aug 2, 2025
2fcfb21
fix(content_management): remove loading row from topics table
fulleni Aug 2, 2025
c800880
refactor(content_management): improve sources table loading state
fulleni Aug 2, 2025
0db0076
feat(router): add archived content page route
fulleni Aug 2, 2025
a83aa11
feat(content_management): add archived items button and update tooltips
fulleni Aug 2, 2025
0c43dc8
feat(content_management): add archived content page
fulleni Aug 2, 2025
1a164cd
feat(router): add routes for archived headlines, topics, and sources
fulleni Aug 2, 2025
392cb69
feat(content_management): add archived content state
fulleni Aug 2, 2025
6c723b6
feat(content_management): add archived content events
fulleni Aug 2, 2025
b00cf36
feat(archived_content): implement ArchivedContentBloc
fulleni Aug 2, 2025
7b8fae0
refactor(content_management): remove unused archived content bloc
fulleni Aug 2, 2025
225c29f
feat(content_management): add archived headlines bloc
fulleni Aug 2, 2025
439d5e0
feat(content_management): add archived topics bloc
fulleni Aug 2, 2025
5a06895
feat(content_management): add archived sources bloc
fulleni Aug 2, 2025
ac2bd91
refactor(content_management): remove archived content page
fulleni Aug 2, 2025
1aee2f4
feat(content_management): navigate to respective archived pages
fulleni Aug 2, 2025
69fddcd
feat(router): add archived content routes and placeholders
fulleni Aug 2, 2025
deb091e
feat(content_management): add archived headlines events
fulleni Aug 2, 2025
c8523f2
refactor(content_management): enhance archived headlines state manage…
fulleni Aug 2, 2025
769861f
build(content_management): add core dependency
fulleni Aug 2, 2025
7d5272d
feat(content_management): implement loading, restoring, and deleting …
fulleni Aug 2, 2025
7a238be
feat(content_management): add ArchivedHeadlinesPage with data table a…
fulleni Aug 2, 2025
8bc91cf
feat(localization): add Arabic and English translations for archived …
fulleni Aug 2, 2025
a67b062
refactor(content_management): update props type in ArchivedTopicsEven…
fulleni Aug 2, 2025
1cf3e96
refactor(content_management): restructure ArchivedTopicsState to incl…
fulleni Aug 2, 2025
9772698
feat(content_management): enhance ArchivedTopicsBloc with pagination …
fulleni Aug 2, 2025
97f5c36
feat(content_management): add ArchivedTopicsPage with pagination and …
fulleni Aug 2, 2025
daa8060
fix(archived_headlines): add missing import for AppLocalizations
fulleni Aug 2, 2025
5059e8a
refactor(archived_sources): update props type in ArchivedSourcesEvent…
fulleni Aug 2, 2025
ca80750
refactor(archived_sources): redefine ArchivedSourcesState with status…
fulleni Aug 2, 2025
7f0eb5f
refactor(archived_sources_bloc): improve error handling in source res…
fulleni Aug 2, 2025
da59c9c
chore: misc
fulleni Aug 2, 2025
13f519e
chore: misc
fulleni Aug 2, 2025
2799f65
feat(localization): add archived topics and sources translations
fulleni Aug 2, 2025
72ebd21
fix(localization): remove TODO comments for localization in archived …
fulleni Aug 2, 2025
66223fe
fix(localization): replace hardcoded strings with localized values in…
fulleni Aug 2, 2025
95536d2
fix(localization): replace hardcoded tooltips with localized values i…
fulleni Aug 2, 2025
1f3c83c
fix(localization): replace hardcoded tooltip with localized value in …
fulleni Aug 2, 2025
1570776
fix(localization): replace hardcoded tooltip with localized value in …
fulleni Aug 2, 2025
600be46
fix(localization): replace hardcoded tooltip with localized value for…
fulleni Aug 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import 'package:bloc/bloc.dart';
import 'package:core/core.dart';
import 'package:data_repository/data_repository.dart';
import 'package:equatable/equatable.dart';

part 'archived_headlines_event.dart';
part 'archived_headlines_state.dart';

class ArchivedHeadlinesBloc
extends Bloc<ArchivedHeadlinesEvent, ArchivedHeadlinesState> {
ArchivedHeadlinesBloc({
required DataRepository<Headline> headlinesRepository,
}) : _headlinesRepository = headlinesRepository,
super(const ArchivedHeadlinesState()) {
on<LoadArchivedHeadlinesRequested>(_onLoadArchivedHeadlinesRequested);
on<RestoreHeadlineRequested>(_onRestoreHeadlineRequested);
on<DeleteHeadlineForeverRequested>(_onDeleteHeadlineForeverRequested);
}

final DataRepository<Headline> _headlinesRepository;

Future<void> _onLoadArchivedHeadlinesRequested(
LoadArchivedHeadlinesRequested event,
Emitter<ArchivedHeadlinesState> emit,
) async {
emit(state.copyWith(status: ArchivedHeadlinesStatus.loading));
try {
final isPaginating = event.startAfterId != null;
final previousHeadlines = isPaginating ? state.headlines : <Headline>[];

final paginatedHeadlines = await _headlinesRepository.readAll(
filter: {'status': ContentStatus.archived.name},
sort: [const SortOption('updatedAt', SortOrder.desc)],
pagination: PaginationOptions(
cursor: event.startAfterId,
limit: event.limit,
),
);
emit(
state.copyWith(
status: ArchivedHeadlinesStatus.success,
headlines: [...previousHeadlines, ...paginatedHeadlines.items],
cursor: paginatedHeadlines.cursor,
hasMore: paginatedHeadlines.hasMore,
),
);
} on HttpException catch (e) {
emit(
state.copyWith(
status: ArchivedHeadlinesStatus.failure,
exception: e,
),
);
} catch (e) {
emit(
state.copyWith(
status: ArchivedHeadlinesStatus.failure,
exception: UnknownException('An unexpected error occurred: $e'),
),
);
}
}

Future<void> _onRestoreHeadlineRequested(
RestoreHeadlineRequested event,
Emitter<ArchivedHeadlinesState> emit,
) async {
final originalHeadlines = List<Headline>.from(state.headlines);
final headlineIndex = originalHeadlines.indexWhere((h) => h.id == event.id);
if (headlineIndex == -1) return;

final headlineToRestore = originalHeadlines[headlineIndex];
final updatedHeadlines = originalHeadlines..removeAt(headlineIndex);

emit(state.copyWith(headlines: updatedHeadlines));

try {
await _headlinesRepository.update(
id: event.id,
item: headlineToRestore.copyWith(status: ContentStatus.active),
);
} on HttpException catch (e) {
emit(state.copyWith(headlines: originalHeadlines, exception: e));
} catch (e) {
emit(
state.copyWith(
headlines: originalHeadlines,
exception: UnknownException('An unexpected error occurred: $e'),
),
);
}
}

Future<void> _onDeleteHeadlineForeverRequested(
DeleteHeadlineForeverRequested event,
Emitter<ArchivedHeadlinesState> emit,
) async {
final originalHeadlines = List<Headline>.from(state.headlines);
final headlineIndex = originalHeadlines.indexWhere((h) => h.id == event.id);
if (headlineIndex == -1) return;

final updatedHeadlines = originalHeadlines..removeAt(headlineIndex);
emit(state.copyWith(headlines: updatedHeadlines));

try {
await _headlinesRepository.delete(id: event.id);
} on HttpException catch (e) {
emit(state.copyWith(headlines: originalHeadlines, exception: e));
} catch (e) {
emit(
state.copyWith(
headlines: originalHeadlines,
exception: UnknownException('An unexpected error occurred: $e'),
),
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
part of 'archived_headlines_bloc.dart';

sealed class ArchivedHeadlinesEvent extends Equatable {
const ArchivedHeadlinesEvent();

@override
List<Object?> get props => [];
}

/// Event to request loading of archived headlines.
final class LoadArchivedHeadlinesRequested extends ArchivedHeadlinesEvent {
const LoadArchivedHeadlinesRequested({this.startAfterId, this.limit});

final String? startAfterId;
final int? limit;

@override
List<Object?> get props => [startAfterId, limit];
}

/// Event to restore an archived headline.
final class RestoreHeadlineRequested extends ArchivedHeadlinesEvent {
const RestoreHeadlineRequested(this.id);

final String id;

@override
List<Object?> get props => [id];
}

/// Event to permanently delete an archived headline.
final class DeleteHeadlineForeverRequested extends ArchivedHeadlinesEvent {
const DeleteHeadlineForeverRequested(this.id);

final String id;

@override
List<Object?> get props => [id];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
part of 'archived_headlines_bloc.dart';

/// Represents the status of archived content operations.
enum ArchivedHeadlinesStatus {
initial,
loading,
success,
failure,
}

/// The state for the archived content feature.
class ArchivedHeadlinesState extends Equatable {
const ArchivedHeadlinesState({
this.status = ArchivedHeadlinesStatus.initial,
this.headlines = const [],
this.cursor,
this.hasMore = false,
this.exception,
});

final ArchivedHeadlinesStatus status;
final List<Headline> headlines;
final String? cursor;
final bool hasMore;
final HttpException? exception;

ArchivedHeadlinesState copyWith({
ArchivedHeadlinesStatus? status,
List<Headline>? headlines,
String? cursor,
bool? hasMore,
HttpException? exception,
}) {
return ArchivedHeadlinesState(
status: status ?? this.status,
headlines: headlines ?? this.headlines,
cursor: cursor ?? this.cursor,
hasMore: hasMore ?? this.hasMore,
exception: exception ?? this.exception,
);
}

@override
List<Object?> get props => [
status,
headlines,
cursor,
hasMore,
exception,
];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import 'package:bloc/bloc.dart';
import 'package:core/core.dart';
import 'package:data_repository/data_repository.dart';
import 'package:equatable/equatable.dart';

part 'archived_sources_event.dart';
part 'archived_sources_state.dart';

class ArchivedSourcesBloc
extends Bloc<ArchivedSourcesEvent, ArchivedSourcesState> {
ArchivedSourcesBloc({
required DataRepository<Source> sourcesRepository,
}) : _sourcesRepository = sourcesRepository,
super(const ArchivedSourcesState()) {
on<LoadArchivedSourcesRequested>(_onLoadArchivedSourcesRequested);
on<RestoreSourceRequested>(_onRestoreSourceRequested);
}

final DataRepository<Source> _sourcesRepository;

Future<void> _onLoadArchivedSourcesRequested(
LoadArchivedSourcesRequested event,
Emitter<ArchivedSourcesState> emit,
) async {
emit(state.copyWith(status: ArchivedSourcesStatus.loading));
try {
final isPaginating = event.startAfterId != null;
final previousSources = isPaginating ? state.sources : <Source>[];

final paginatedSources = await _sourcesRepository.readAll(
filter: {'status': ContentStatus.archived.name},
sort: [const SortOption('updatedAt', SortOrder.desc)],
pagination: PaginationOptions(
cursor: event.startAfterId,
limit: event.limit,
),
);
emit(
state.copyWith(
status: ArchivedSourcesStatus.success,
sources: [...previousSources, ...paginatedSources.items],
cursor: paginatedSources.cursor,
hasMore: paginatedSources.hasMore,
),
);
} on HttpException catch (e) {
emit(
state.copyWith(
status: ArchivedSourcesStatus.failure,
exception: e,
),
);
} catch (e) {
emit(
state.copyWith(
status: ArchivedSourcesStatus.failure,
exception: UnknownException('An unexpected error occurred: $e'),
),
);
}
}

Future<void> _onRestoreSourceRequested(
RestoreSourceRequested event,
Emitter<ArchivedSourcesState> emit,
) async {
final originalSources = List<Source>.from(state.sources);
final sourceIndex = originalSources.indexWhere((s) => s.id == event.id);
if (sourceIndex == -1) return;

final sourceToRestore = originalSources[sourceIndex];
final updatedSources = originalSources..removeAt(sourceIndex);

emit(state.copyWith(sources: updatedSources));

try {
await _sourcesRepository.update(
id: event.id,
item: sourceToRestore.copyWith(status: ContentStatus.active),
);
} on HttpException catch (e) {
emit(state.copyWith(sources: originalSources, exception: e));
} catch (e) {
emit(
state.copyWith(
sources: originalSources,
exception: UnknownException('An unexpected error occurred: $e'),
),
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
part of 'archived_sources_bloc.dart';

sealed class ArchivedSourcesEvent extends Equatable {
const ArchivedSourcesEvent();

@override
List<Object?> get props => [];
}

/// Event to request loading of archived sources.
final class LoadArchivedSourcesRequested extends ArchivedSourcesEvent {
const LoadArchivedSourcesRequested({this.startAfterId, this.limit});

final String? startAfterId;
final int? limit;

@override
List<Object?> get props => [startAfterId, limit];
}

/// Event to restore an archived source.
final class RestoreSourceRequested extends ArchivedSourcesEvent {
const RestoreSourceRequested(this.id);

final String id;

@override
List<Object?> get props => [id];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
part of 'archived_sources_bloc.dart';

/// Represents the status of archived content operations.
enum ArchivedSourcesStatus {
initial,
loading,
success,
failure,
}

/// The state for the archived content feature.
class ArchivedSourcesState extends Equatable {
const ArchivedSourcesState({
this.status = ArchivedSourcesStatus.initial,
this.sources = const [],
this.cursor,
this.hasMore = false,
this.exception,
});

final ArchivedSourcesStatus status;
final List<Source> sources;
final String? cursor;
final bool hasMore;
final HttpException? exception;

ArchivedSourcesState copyWith({
ArchivedSourcesStatus? status,
List<Source>? sources,
String? cursor,
bool? hasMore,
HttpException? exception,
}) {
return ArchivedSourcesState(
status: status ?? this.status,
sources: sources ?? this.sources,
cursor: cursor ?? this.cursor,
hasMore: hasMore ?? this.hasMore,
exception: exception ?? this.exception,
);
}

@override
List<Object?> get props => [
status,
sources,
cursor,
hasMore,
exception,
];
}
Loading
Loading