mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
fix: modify blocks pb
This commit is contained in:
commit
4582413e89
@ -0,0 +1,24 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:appflowy/core/notification_helper.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-document2/notification.pb.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
|
||||||
|
typedef DocumentNotificationCallback = void Function(
|
||||||
|
DocumentNotification,
|
||||||
|
Either<Uint8List, FlowyError>,
|
||||||
|
);
|
||||||
|
|
||||||
|
class DocumentNotificationParser
|
||||||
|
extends NotificationParser<DocumentNotification, FlowyError> {
|
||||||
|
DocumentNotificationParser({
|
||||||
|
String? id,
|
||||||
|
required DocumentNotificationCallback callback,
|
||||||
|
}) : super(
|
||||||
|
id: id,
|
||||||
|
callback: callback,
|
||||||
|
tyParser: (ty) => DocumentNotification.valueOf(ty),
|
||||||
|
errorParser: (bytes) => FlowyError.fromBuffer(bytes),
|
||||||
|
);
|
||||||
|
}
|
@ -3,6 +3,7 @@ import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_node_
|
|||||||
import 'package:appflowy/plugins/trash/application/trash_service.dart';
|
import 'package:appflowy/plugins/trash/application/trash_service.dart';
|
||||||
import 'package:appflowy/user/application/user_service.dart';
|
import 'package:appflowy/user/application/user_service.dart';
|
||||||
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
||||||
|
import 'package:appflowy/workspace/application/doc/doc_listener.dart';
|
||||||
import 'package:appflowy/plugins/document/application/doc_service.dart';
|
import 'package:appflowy/plugins/document/application/doc_service.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pbserver.dart';
|
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pbserver.dart';
|
||||||
@ -17,12 +18,13 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
|||||||
import 'package:dartz/dartz.dart';
|
import 'package:dartz/dartz.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:appflowy/util/either_extension.dart';
|
import 'package:appflowy/util/either_extension.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-document2/entities.pb.dart';
|
||||||
part 'doc_bloc.freezed.dart';
|
part 'doc_bloc.freezed.dart';
|
||||||
|
|
||||||
class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||||
final ViewPB view;
|
final ViewPB view;
|
||||||
final DocumentService _documentService;
|
final DocumentService _documentService;
|
||||||
|
final DocumentListener _docListener;
|
||||||
|
|
||||||
final ViewListener _listener;
|
final ViewListener _listener;
|
||||||
final TrashService _trashService;
|
final TrashService _trashService;
|
||||||
@ -32,12 +34,14 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
|||||||
DocumentBloc({
|
DocumentBloc({
|
||||||
required this.view,
|
required this.view,
|
||||||
}) : _documentService = DocumentService(),
|
}) : _documentService = DocumentService(),
|
||||||
|
_docListener = DocumentListener(id: view.id),
|
||||||
_listener = ViewListener(view: view),
|
_listener = ViewListener(view: view),
|
||||||
_trashService = TrashService(),
|
_trashService = TrashService(),
|
||||||
super(DocumentState.initial()) {
|
super(DocumentState.initial()) {
|
||||||
on<DocumentEvent>((event, emit) async {
|
on<DocumentEvent>((event, emit) async {
|
||||||
await event.map(
|
await event.map(
|
||||||
initial: (Initial value) async {
|
initial: (Initial value) async {
|
||||||
|
_listenOnDocChange();
|
||||||
await _initial(value, emit);
|
await _initial(value, emit);
|
||||||
_listenOnViewChange();
|
_listenOnViewChange();
|
||||||
},
|
},
|
||||||
@ -73,6 +77,7 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await _documentService.closeDocument(docId: view.id);
|
await _documentService.closeDocument(docId: view.id);
|
||||||
|
await _documentService.closeDocumentV2(view: view);
|
||||||
return super.close();
|
return super.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,6 +93,39 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
final result = await _documentService.openDocument(view: view);
|
final result = await _documentService.openDocument(view: view);
|
||||||
|
// test code
|
||||||
|
final document = await _documentService.openDocumentV2(view: view);
|
||||||
|
BlockPB? root;
|
||||||
|
document.fold((l) {
|
||||||
|
print('---------<open document v2>-----------');
|
||||||
|
print('page id = ${l.pageId}');
|
||||||
|
l.blocks.blocks.forEach((key, value) {
|
||||||
|
print('-----<block begin>-----');
|
||||||
|
print('block = $value');
|
||||||
|
if (value.ty == 'page') {
|
||||||
|
root = value;
|
||||||
|
}
|
||||||
|
print('-----<block end>-----');
|
||||||
|
});
|
||||||
|
print('---------<open document v2>-----------');
|
||||||
|
}, (r) {});
|
||||||
|
if (root != null) {
|
||||||
|
await _documentService.applyAction(
|
||||||
|
view: view,
|
||||||
|
actions: [
|
||||||
|
BlockActionPB(
|
||||||
|
action: BlockActionTypePB.Insert,
|
||||||
|
payload: BlockActionPayloadPB(
|
||||||
|
block: BlockPB()
|
||||||
|
..id = 'id_0'
|
||||||
|
..ty = 'text'
|
||||||
|
..parentId = root!.id,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return result.fold(
|
return result.fold(
|
||||||
(documentData) async {
|
(documentData) async {
|
||||||
await _initEditorState(documentData).whenComplete(() {
|
await _initEditorState(documentData).whenComplete(() {
|
||||||
@ -126,6 +164,14 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _listenOnDocChange() {
|
||||||
|
_docListener.start(
|
||||||
|
didReceiveUpdate: () {
|
||||||
|
print('---------<receive document update>-----------');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _initEditorState(DocumentDataPB documentData) async {
|
Future<void> _initEditorState(DocumentDataPB documentData) async {
|
||||||
final document = Document.fromJson(jsonDecode(documentData.content));
|
final document = Document.fromJson(jsonDecode(documentData.content));
|
||||||
final editorState = EditorState(document: document);
|
final editorState = EditorState(document: document);
|
||||||
|
@ -4,6 +4,7 @@ import 'package:appflowy_backend/dispatch/dispatch.dart';
|
|||||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-document2/entities.pb.dart';
|
||||||
|
|
||||||
class DocumentService {
|
class DocumentService {
|
||||||
Future<Either<DocumentDataPB, FlowyError>> openDocument({
|
Future<Either<DocumentDataPB, FlowyError>> openDocument({
|
||||||
@ -39,4 +40,32 @@ class DocumentService {
|
|||||||
final payload = ViewIdPB(value: docId);
|
final payload = ViewIdPB(value: docId);
|
||||||
return FolderEventCloseView(payload).send();
|
return FolderEventCloseView(payload).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Either<DocumentDataPB2, FlowyError>> openDocumentV2({
|
||||||
|
required ViewPB view,
|
||||||
|
}) async {
|
||||||
|
await FolderEventSetLatestView(ViewIdPB(value: view.id)).send();
|
||||||
|
|
||||||
|
final payload = OpenDocumentPayloadPBV2()..documentId = view.id;
|
||||||
|
|
||||||
|
return DocumentEvent2OpenDocument(payload).send();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Either<Unit, FlowyError>> closeDocumentV2({
|
||||||
|
required ViewPB view,
|
||||||
|
}) async {
|
||||||
|
final payload = CloseDocumentPayloadPBV2()..documentId = view.id;
|
||||||
|
return DocumentEvent2CloseDocument(payload).send();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Either<Unit, FlowyError>> applyAction({
|
||||||
|
required ViewPB view,
|
||||||
|
required List<BlockActionPB> actions,
|
||||||
|
}) async {
|
||||||
|
final payload = ApplyActionPayloadPBV2(
|
||||||
|
documentId: view.id,
|
||||||
|
actions: actions,
|
||||||
|
);
|
||||||
|
return DocumentEvent2ApplyAction(payload).send();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,54 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'package:appflowy/core/document_notification.dart';
|
||||||
|
import 'package:dartz/dartz.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-document2/notification.pb.dart';
|
||||||
|
import 'package:appflowy_backend/rust_stream.dart';
|
||||||
|
import 'package:flowy_infra/notifier.dart';
|
||||||
|
|
||||||
|
class DocumentListener {
|
||||||
|
DocumentListener({
|
||||||
|
required this.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
final _didReceiveUpdate = PublishNotifier();
|
||||||
|
StreamSubscription<SubscribeObject>? _subscription;
|
||||||
|
DocumentNotificationParser? _parser;
|
||||||
|
|
||||||
|
Function()? didReceiveUpdate;
|
||||||
|
|
||||||
|
void start({
|
||||||
|
void Function()? didReceiveUpdate,
|
||||||
|
}) {
|
||||||
|
this.didReceiveUpdate = didReceiveUpdate;
|
||||||
|
|
||||||
|
_parser = DocumentNotificationParser(
|
||||||
|
id: id,
|
||||||
|
callback: _callback,
|
||||||
|
);
|
||||||
|
_subscription = RustStreamReceiver.listen(
|
||||||
|
(observable) => _parser?.parse(observable),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _callback(
|
||||||
|
DocumentNotification ty,
|
||||||
|
Either<Uint8List, FlowyError> result,
|
||||||
|
) {
|
||||||
|
switch (ty) {
|
||||||
|
case DocumentNotification.DidReceiveUpdate:
|
||||||
|
didReceiveUpdate?.call();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> stop() async {
|
||||||
|
await _subscription?.cancel();
|
||||||
|
}
|
||||||
|
}
|
@ -17,6 +17,7 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
|||||||
import 'package:appflowy_backend/protobuf/dart-ffi/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/dart-ffi/protobuf.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart';
|
||||||
|
|
||||||
// ignore: unused_import
|
// ignore: unused_import
|
||||||
@ -30,6 +31,7 @@ part 'dart_event/flowy-net/dart_event.dart';
|
|||||||
part 'dart_event/flowy-user/dart_event.dart';
|
part 'dart_event/flowy-user/dart_event.dart';
|
||||||
part 'dart_event/flowy-database/dart_event.dart';
|
part 'dart_event/flowy-database/dart_event.dart';
|
||||||
part 'dart_event/flowy-document/dart_event.dart';
|
part 'dart_event/flowy-document/dart_event.dart';
|
||||||
|
part 'dart_event/flowy-document2/dart_event.dart';
|
||||||
|
|
||||||
enum FFIException {
|
enum FFIException {
|
||||||
RequestIsEmpty,
|
RequestIsEmpty,
|
||||||
|
1241
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
1241
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -36,9 +36,13 @@ custom-protocol = ["tauri/custom-protocol"]
|
|||||||
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab" }
|
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab" }
|
||||||
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab" }
|
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab" }
|
||||||
collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab" }
|
collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab" }
|
||||||
|
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab" }
|
||||||
|
|
||||||
#collab = { path = "../../AppFlowy-Collab/collab" }
|
#collab = { path = "../../AppFlowy-Collab/collab" }
|
||||||
#collab-folder = { path = "../../AppFlowy-Collab/collab-folder" }
|
#collab-folder = { path = "../../AppFlowy-Collab/collab-folder" }
|
||||||
#collab-persistence = { path = "../../AppFlowy-Collab/collab-persistence" }
|
#collab-persistence = { path = "../../AppFlowy-Collab/collab-persistence" }
|
||||||
|
#collab-document = { path = "../../AppFlowy-Collab/collab-document" }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Routes, Route, BrowserRouter } from 'react-router-dom';
|
import { Routes, Route, BrowserRouter } from 'react-router-dom';
|
||||||
|
|
||||||
import { TestColors } from './components/TestColors/TestColors';
|
import { ColorPalette } from './components/tests/ColorPalette';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { store } from './stores/store';
|
import { store } from './stores/store';
|
||||||
import { DocumentPage } from './views/DocumentPage';
|
import { DocumentPage } from './views/DocumentPage';
|
||||||
@ -14,6 +14,8 @@ import { ErrorHandlerPage } from './components/error/ErrorHandlerPage';
|
|||||||
import initializeI18n from './stores/i18n/initializeI18n';
|
import initializeI18n from './stores/i18n/initializeI18n';
|
||||||
import { TestAPI } from './components/tests/TestAPI';
|
import { TestAPI } from './components/tests/TestAPI';
|
||||||
import { GetStarted } from './components/auth/GetStarted/GetStarted';
|
import { GetStarted } from './components/auth/GetStarted/GetStarted';
|
||||||
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
|
import { AllIcons } from '$app/components/tests/AllIcons';
|
||||||
|
|
||||||
initializeI18n();
|
initializeI18n();
|
||||||
|
|
||||||
@ -21,20 +23,22 @@ const App = () => {
|
|||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<Routes>
|
<ErrorBoundary FallbackComponent={ErrorHandlerPage}>
|
||||||
<Route path={'/'} element={<ProtectedRoutes />}>
|
<Routes>
|
||||||
<Route path={'/page/colors'} element={<TestColors />} />
|
<Route path={'/'} element={<ProtectedRoutes />}>
|
||||||
<Route path={'/page/api-test'} element={<TestAPI />} />
|
<Route path={'/page/all-icons'} element={<AllIcons />} />
|
||||||
<Route path={'/page/document/:id'} element={<DocumentPage />} />
|
<Route path={'/page/colors'} element={<ColorPalette />} />
|
||||||
<Route path={'/page/board/:id'} element={<BoardPage />} />
|
<Route path={'/page/api-test'} element={<TestAPI />} />
|
||||||
<Route path={'/page/grid/:id'} element={<GridPage />} />
|
<Route path={'/page/document/:id'} element={<DocumentPage />} />
|
||||||
</Route>
|
<Route path={'/page/board/:id'} element={<BoardPage />} />
|
||||||
<Route path={'/auth/login'} element={<LoginPage />}></Route>
|
<Route path={'/page/grid/:id'} element={<GridPage />} />
|
||||||
<Route path={'/auth/getStarted'} element={<GetStarted />}></Route>
|
</Route>
|
||||||
<Route path={'/auth/signUp'} element={<SignUpPage />}></Route>
|
<Route path={'/auth/login'} element={<LoginPage />}></Route>
|
||||||
<Route path={'/auth/confirm-account'} element={<ConfirmAccountPage />}></Route>
|
<Route path={'/auth/getStarted'} element={<GetStarted />}></Route>
|
||||||
</Routes>
|
<Route path={'/auth/signUp'} element={<SignUpPage />}></Route>
|
||||||
<ErrorHandlerPage></ErrorHandlerPage>
|
<Route path={'/auth/confirm-account'} element={<ConfirmAccountPage />}></Route>
|
||||||
|
</Routes>
|
||||||
|
</ErrorBoundary>
|
||||||
</Provider>
|
</Provider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
);
|
);
|
||||||
|
@ -1,44 +0,0 @@
|
|||||||
export const TestColors = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h2 className={'mb-4'}>Main</h2>
|
|
||||||
<div className={'mb-8 flex flex-wrap items-center'}>
|
|
||||||
<div className={'m-2 h-[100px] w-[100px] bg-main-accent'}></div>
|
|
||||||
<div className={'m-2 h-[100px] w-[100px] bg-main-hovered'}></div>
|
|
||||||
<div className={'m-2 h-[100px] w-[100px] bg-main-secondary'}></div>
|
|
||||||
<div className={'m-2 h-[100px] w-[100px] bg-main-selector'}></div>
|
|
||||||
<div className={'m-2 h-[100px] w-[100px] bg-main-alert'}></div>
|
|
||||||
<div className={'m-2 h-[100px] w-[100px] bg-main-warning'}></div>
|
|
||||||
<div className={'m-2 h-[100px] w-[100px] bg-main-success'}></div>
|
|
||||||
</div>
|
|
||||||
<h2 className={'mb-4'}>Tint</h2>
|
|
||||||
<div className={'mb-8 flex flex-wrap items-center'}>
|
|
||||||
<div className={'m-2 h-[100px] w-[100px] bg-tint-1'}></div>
|
|
||||||
<div className={'m-2 h-[100px] w-[100px] bg-tint-2'}></div>
|
|
||||||
<div className={'m-2 h-[100px] w-[100px] bg-tint-3'}></div>
|
|
||||||
<div className={'m-2 h-[100px] w-[100px] bg-tint-4'}></div>
|
|
||||||
<div className={'m-2 h-[100px] w-[100px] bg-tint-5'}></div>
|
|
||||||
<div className={'m-2 h-[100px] w-[100px] bg-tint-6'}></div>
|
|
||||||
<div className={'m-2 h-[100px] w-[100px] bg-tint-7'}></div>
|
|
||||||
<div className={'m-2 h-[100px] w-[100px] bg-tint-8'}></div>
|
|
||||||
<div className={'m-2 h-[100px] w-[100px] bg-tint-9'}></div>
|
|
||||||
</div>
|
|
||||||
<h2 className={'mb-4'}>Shades</h2>
|
|
||||||
<div className={'mb-8 flex flex-wrap items-center'}>
|
|
||||||
<div className={'m-2 h-[100px] w-[100px] bg-shade-1'}></div>
|
|
||||||
<div className={'m-2 h-[100px] w-[100px] bg-shade-2'}></div>
|
|
||||||
<div className={'m-2 h-[100px] w-[100px] bg-shade-3'}></div>
|
|
||||||
<div className={'m-2 h-[100px] w-[100px] bg-shade-4'}></div>
|
|
||||||
<div className={'m-2 h-[100px] w-[100px] bg-shade-5'}></div>
|
|
||||||
<div className={'m-2 h-[100px] w-[100px] bg-shade-6'}></div>
|
|
||||||
</div>
|
|
||||||
<h2 className={'mb-4'}>Surface</h2>
|
|
||||||
<div className={'mb-8 flex flex-wrap items-center'}>
|
|
||||||
<div className={'m-2 h-[100px] w-[100px] bg-surface-1'}></div>
|
|
||||||
<div className={'m-2 h-[100px] w-[100px] bg-surface-2'}></div>
|
|
||||||
<div className={'m-2 h-[100px] w-[100px] bg-surface-3'}></div>
|
|
||||||
<div className={'bg-surface-4 m-2 h-[100px] w-[100px]'}></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -21,7 +21,7 @@ export const CellOptions = ({
|
|||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
onClick={() => onClick()}
|
onClick={() => onClick()}
|
||||||
className={'flex flex-wrap items-center gap-2 px-4 py-2 text-xs text-black'}
|
className={'flex w-full flex-wrap items-center gap-2 px-4 py-2 text-xs text-black'}
|
||||||
>
|
>
|
||||||
{data?.select_options?.map((option, index) => (
|
{data?.select_options?.map((option, index) => (
|
||||||
<div className={`${getBgColor(option.color)} rounded px-2 py-0.5`} key={index}>
|
<div className={`${getBgColor(option.color)} rounded px-2 py-0.5`} key={index}>
|
||||||
|
@ -9,10 +9,10 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { Details2Svg } from '$app/components/_shared/svg/Details2Svg';
|
import { Details2Svg } from '$app/components/_shared/svg/Details2Svg';
|
||||||
import { CheckmarkSvg } from '$app/components/_shared/svg/CheckmarkSvg';
|
import { CheckmarkSvg } from '$app/components/_shared/svg/CheckmarkSvg';
|
||||||
import { CloseSvg } from '$app/components/_shared/svg/CloseSvg';
|
import { CloseSvg } from '$app/components/_shared/svg/CloseSvg';
|
||||||
import useOutsideClick from '$app/components/_shared/useOutsideClick';
|
|
||||||
import { SelectOptionCellBackendService } from '$app/stores/effects/database/cell/select_option_bd_svc';
|
import { SelectOptionCellBackendService } from '$app/stores/effects/database/cell/select_option_bd_svc';
|
||||||
import { useAppSelector } from '$app/stores/store';
|
import { useAppSelector } from '$app/stores/store';
|
||||||
import { ISelectOptionType } from '$app/stores/reducers/database/slice';
|
import { ISelectOption, ISelectOptionType } from '$app/stores/reducers/database/slice';
|
||||||
|
import { PopupWindow } from '$app/components/_shared/PopupWindow';
|
||||||
|
|
||||||
export const CellOptionsPopup = ({
|
export const CellOptionsPopup = ({
|
||||||
top,
|
top,
|
||||||
@ -21,6 +21,7 @@ export const CellOptionsPopup = ({
|
|||||||
cellCache,
|
cellCache,
|
||||||
fieldController,
|
fieldController,
|
||||||
onOutsideClick,
|
onOutsideClick,
|
||||||
|
openOptionDetail,
|
||||||
}: {
|
}: {
|
||||||
top: number;
|
top: number;
|
||||||
left: number;
|
left: number;
|
||||||
@ -28,27 +29,19 @@ export const CellOptionsPopup = ({
|
|||||||
cellCache: CellCache;
|
cellCache: CellCache;
|
||||||
fieldController: FieldController;
|
fieldController: FieldController;
|
||||||
onOutsideClick: () => void;
|
onOutsideClick: () => void;
|
||||||
|
openOptionDetail: (_left: number, _top: number, _select_option: SelectOptionPB) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const { t } = useTranslation('');
|
const { t } = useTranslation('');
|
||||||
const [adjustedTop, setAdjustedTop] = useState(-100);
|
|
||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState('');
|
||||||
const { data, cellController } = useCell(cellIdentifier, cellCache, fieldController);
|
const { data } = useCell(cellIdentifier, cellCache, fieldController);
|
||||||
const databaseStore = useAppSelector((state) => state.database);
|
const databaseStore = useAppSelector((state) => state.database);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ref.current) return;
|
if (inputRef?.current) {
|
||||||
const { height } = ref.current.getBoundingClientRect();
|
inputRef.current.focus();
|
||||||
if (top + height + 40 > window.innerHeight) {
|
|
||||||
setAdjustedTop(window.innerHeight - height - 40);
|
|
||||||
} else {
|
|
||||||
setAdjustedTop(top);
|
|
||||||
}
|
}
|
||||||
}, [ref, window, top, left]);
|
}, [inputRef]);
|
||||||
|
|
||||||
useOutsideClick(ref, async () => {
|
|
||||||
onOutsideClick();
|
|
||||||
});
|
|
||||||
|
|
||||||
const onKeyDown: KeyboardEventHandler = async (e) => {
|
const onKeyDown: KeyboardEventHandler = async (e) => {
|
||||||
if (e.key === 'Enter' && value.length > 0) {
|
if (e.key === 'Enter' && value.length > 0) {
|
||||||
@ -63,11 +56,7 @@ export const CellOptionsPopup = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onToggleOptionClick = async (option: SelectOptionPB) => {
|
const onToggleOptionClick = async (option: SelectOptionPB) => {
|
||||||
if (
|
if ((data as SelectOptionCellDataPB)?.select_options?.find((selectedOption) => selectedOption.id === option.id)) {
|
||||||
(data as SelectOptionCellDataPB | undefined)?.select_options?.find(
|
|
||||||
(selectedOption) => selectedOption.id === option.id
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
await new SelectOptionCellBackendService(cellIdentifier).unselectOption([option.id]);
|
await new SelectOptionCellBackendService(cellIdentifier).unselectOption([option.id]);
|
||||||
} else {
|
} else {
|
||||||
await new SelectOptionCellBackendService(cellIdentifier).selectOption([option.id]);
|
await new SelectOptionCellBackendService(cellIdentifier).selectOption([option.id]);
|
||||||
@ -75,23 +64,37 @@ export const CellOptionsPopup = ({
|
|||||||
setValue('');
|
setValue('');
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const onKeyDownWrapper: KeyboardEventHandler = (e) => {
|
||||||
console.log('loaded data: ', data);
|
if (e.key === 'Escape') {
|
||||||
console.log('have stored ', databaseStore.fields[cellIdentifier.fieldId]);
|
onOutsideClick();
|
||||||
}, [data]);
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onOptionDetailClick = (e: any, option: ISelectOption) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
let target = e.target as HTMLElement;
|
||||||
|
|
||||||
|
while (!(target instanceof HTMLButtonElement)) {
|
||||||
|
if (target.parentElement === null) return;
|
||||||
|
target = target.parentElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectOption = new SelectOptionPB({
|
||||||
|
id: option.selectOptionId,
|
||||||
|
name: option.title,
|
||||||
|
color: option.color || SelectOptionColorPB.Purple,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { right: _left, top: _top } = target.getBoundingClientRect();
|
||||||
|
openOptionDetail(_left, _top, selectOption);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<PopupWindow className={'p-2 text-xs'} onOutsideClick={onOutsideClick} left={left} top={top}>
|
||||||
ref={ref}
|
<div onKeyDown={onKeyDownWrapper} className={'flex flex-col gap-2 p-2'}>
|
||||||
className={`fixed z-10 rounded-lg bg-white px-2 py-2 text-xs shadow-md transition-opacity duration-300 ${
|
|
||||||
adjustedTop === -100 ? 'opacity-0' : 'opacity-100'
|
|
||||||
}`}
|
|
||||||
style={{ top: `${adjustedTop + 40}px`, left: `${left}px` }}
|
|
||||||
>
|
|
||||||
<div className={'flex flex-col gap-2 p-2'}>
|
|
||||||
<div className={'border-shades-3 flex flex-1 items-center gap-2 rounded border bg-main-selector px-2 '}>
|
<div className={'border-shades-3 flex flex-1 items-center gap-2 rounded border bg-main-selector px-2 '}>
|
||||||
<div className={'flex flex-wrap items-center gap-2 text-black'}>
|
<div className={'flex flex-wrap items-center gap-2 text-black'}>
|
||||||
{(data as SelectOptionCellDataPB | undefined)?.select_options?.map((option, index) => (
|
{(data as SelectOptionCellDataPB)?.select_options?.map((option, index) => (
|
||||||
<div className={`${getBgColor(option.color)} flex items-center gap-0.5 rounded px-1 py-0.5`} key={index}>
|
<div className={`${getBgColor(option.color)} flex items-center gap-0.5 rounded px-1 py-0.5`} key={index}>
|
||||||
<span>{option?.name || ''}</span>
|
<span>{option?.name || ''}</span>
|
||||||
<button onClick={() => onUnselectOptionClick(option)} className={'h-5 w-5 cursor-pointer'}>
|
<button onClick={() => onUnselectOptionClick(option)} className={'h-5 w-5 cursor-pointer'}>
|
||||||
@ -101,6 +104,7 @@ export const CellOptionsPopup = ({
|
|||||||
)) || ''}
|
)) || ''}
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
|
ref={inputRef}
|
||||||
className={'py-2'}
|
className={'py-2'}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => setValue(e.target.value)}
|
onChange={(e) => setValue(e.target.value)}
|
||||||
@ -110,7 +114,7 @@ export const CellOptionsPopup = ({
|
|||||||
<div className={'font-mono text-shade-3'}>{value.length}/30</div>
|
<div className={'font-mono text-shade-3'}>{value.length}/30</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={'-mx-4 h-[1px] bg-shade-6'}></div>
|
<div className={'-mx-4 h-[1px] bg-shade-6'}></div>
|
||||||
<div className={'font-semibold text-shade-3'}>{t('grid.selectOption.panelTitle') || ''}</div>
|
<div className={'font-medium text-shade-3'}>{t('grid.selectOption.panelTitle') || ''}</div>
|
||||||
<div className={'flex flex-col gap-1'}>
|
<div className={'flex flex-col gap-1'}>
|
||||||
{(databaseStore.fields[cellIdentifier.fieldId]?.fieldOptions as ISelectOptionType).selectOptions.map(
|
{(databaseStore.fields[cellIdentifier.fieldId]?.fieldOptions as ISelectOptionType).selectOptions.map(
|
||||||
(option, index) => (
|
(option, index) => (
|
||||||
@ -131,14 +135,14 @@ export const CellOptionsPopup = ({
|
|||||||
>
|
>
|
||||||
<div className={`${getBgColor(option.color)} rounded px-2 py-0.5`}>{option.title}</div>
|
<div className={`${getBgColor(option.color)} rounded px-2 py-0.5`}>{option.title}</div>
|
||||||
<div className={'flex items-center'}>
|
<div className={'flex items-center'}>
|
||||||
{(data as SelectOptionCellDataPB | undefined)?.select_options?.find(
|
{(data as SelectOptionCellDataPB)?.select_options?.find(
|
||||||
(selectedOption) => selectedOption.id === option.selectOptionId
|
(selectedOption) => selectedOption.id === option.selectOptionId
|
||||||
) && (
|
) && (
|
||||||
<button className={'h-5 w-5 p-1'}>
|
<button className={'h-5 w-5 p-1'}>
|
||||||
<CheckmarkSvg></CheckmarkSvg>
|
<CheckmarkSvg></CheckmarkSvg>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button className={'h-6 w-6 p-1'}>
|
<button onClick={(e) => onOptionDetailClick(e, option)} className={'h-6 w-6 p-1'}>
|
||||||
<Details2Svg></Details2Svg>
|
<Details2Svg></Details2Svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -147,6 +151,6 @@ export const CellOptionsPopup = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</PopupWindow>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { FieldType } from '@/services/backend';
|
import { FieldType } from '@/services/backend';
|
||||||
import { FieldTypeIcon } from '$app/components/_shared/EditRow/FieldTypeIcon';
|
import { FieldTypeIcon } from '$app/components/_shared/EditRow/FieldTypeIcon';
|
||||||
import { FieldTypeName } from '$app/components/_shared/EditRow/FieldTypeName';
|
import { FieldTypeName } from '$app/components/_shared/EditRow/FieldTypeName';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { PopupWindow } from '$app/components/_shared/PopupWindow';
|
||||||
import useOutsideClick from '$app/components/_shared/useOutsideClick';
|
|
||||||
|
|
||||||
const typesOrder: FieldType[] = [
|
const typesOrder: FieldType[] = [
|
||||||
FieldType.RichText,
|
FieldType.RichText,
|
||||||
@ -17,39 +16,17 @@ const typesOrder: FieldType[] = [
|
|||||||
|
|
||||||
export const ChangeFieldTypePopup = ({
|
export const ChangeFieldTypePopup = ({
|
||||||
top,
|
top,
|
||||||
right,
|
left,
|
||||||
onClick,
|
onClick,
|
||||||
onOutsideClick,
|
onOutsideClick,
|
||||||
}: {
|
}: {
|
||||||
top: number;
|
top: number;
|
||||||
right: number;
|
left: number;
|
||||||
onClick: (newType: FieldType) => void;
|
onClick: (newType: FieldType) => void;
|
||||||
onOutsideClick: () => void;
|
onOutsideClick: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
|
||||||
const [adjustedTop, setAdjustedTop] = useState(-100);
|
|
||||||
useOutsideClick(ref, async () => {
|
|
||||||
onOutsideClick();
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!ref.current) return;
|
|
||||||
const { height } = ref.current.getBoundingClientRect();
|
|
||||||
if (top + height > window.innerHeight) {
|
|
||||||
setAdjustedTop(window.innerHeight - height);
|
|
||||||
} else {
|
|
||||||
setAdjustedTop(top);
|
|
||||||
}
|
|
||||||
}, [ref, window, top, right]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<PopupWindow className={'p-2 text-xs'} onOutsideClick={onOutsideClick} left={left} top={top}>
|
||||||
ref={ref}
|
|
||||||
className={`fixed z-10 rounded-lg bg-white p-2 text-xs shadow-md transition-opacity duration-300 ${
|
|
||||||
adjustedTop === -100 ? 'opacity-0' : 'opacity-100'
|
|
||||||
}`}
|
|
||||||
style={{ top: `${adjustedTop}px`, left: `${right + 30}px` }}
|
|
||||||
>
|
|
||||||
<div className={'flex flex-col'}>
|
<div className={'flex flex-col'}>
|
||||||
{typesOrder.map((t, i) => (
|
{typesOrder.map((t, i) => (
|
||||||
<button
|
<button
|
||||||
@ -66,6 +43,6 @@ export const ChangeFieldTypePopup = ({
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</PopupWindow>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
|
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
|
||||||
import { CellCache } from '$app/stores/effects/database/cell/cell_cache';
|
import { CellCache } from '$app/stores/effects/database/cell/cell_cache';
|
||||||
import { FieldController } from '$app/stores/effects/database/field/field_controller';
|
import { FieldController } from '$app/stores/effects/database/field/field_controller';
|
||||||
import useOutsideClick from '$app/components/_shared/useOutsideClick';
|
|
||||||
import Calendar from 'react-calendar';
|
import Calendar from 'react-calendar';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { ClockSvg } from '$app/components/_shared/svg/ClockSvg';
|
import { ClockSvg } from '$app/components/_shared/svg/ClockSvg';
|
||||||
import { MoreSvg } from '$app/components/_shared/svg/MoreSvg';
|
import { MoreSvg } from '$app/components/_shared/svg/MoreSvg';
|
||||||
import { EditorUncheckSvg } from '$app/components/_shared/svg/EditorUncheckSvg';
|
import { EditorUncheckSvg } from '$app/components/_shared/svg/EditorUncheckSvg';
|
||||||
import { useCell } from '$app/components/_shared/database-hooks/useCell';
|
import { useCell } from '$app/components/_shared/database-hooks/useCell';
|
||||||
|
import { CalendarData } from '$app/stores/effects/database/cell/controller_builder';
|
||||||
|
import { DateCellDataPB } from '@/services/backend';
|
||||||
|
import { PopupWindow } from '$app/components/_shared/PopupWindow';
|
||||||
|
|
||||||
export const DatePickerPopup = ({
|
export const DatePickerPopup = ({
|
||||||
left,
|
left,
|
||||||
@ -27,47 +29,27 @@ export const DatePickerPopup = ({
|
|||||||
onOutsideClick: () => void;
|
onOutsideClick: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { data, cellController } = useCell(cellIdentifier, cellCache, fieldController);
|
const { data, cellController } = useCell(cellIdentifier, cellCache, fieldController);
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
|
||||||
const [adjustedTop, setAdjustedTop] = useState(-100);
|
|
||||||
// const [value, setValue] = useState();
|
|
||||||
const { t } = useTranslation('');
|
const { t } = useTranslation('');
|
||||||
const [selectedDate, setSelectedDate] = useState<Date>(new Date());
|
const [selectedDate, setSelectedDate] = useState<Date>(new Date());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ref.current) return;
|
const date_pb = data as DateCellDataPB | undefined;
|
||||||
const { height } = ref.current.getBoundingClientRect();
|
if (!date_pb || !date_pb?.date.length) return;
|
||||||
if (top + height + 40 > window.innerHeight) {
|
|
||||||
setAdjustedTop(top - height - 40);
|
|
||||||
} else {
|
|
||||||
setAdjustedTop(top);
|
|
||||||
}
|
|
||||||
}, [ref, window, top, left]);
|
|
||||||
|
|
||||||
useOutsideClick(ref, async () => {
|
// should be changed after we can modify date format
|
||||||
onOutsideClick();
|
setSelectedDate(dayjs(date_pb.date, 'MMM DD, YYYY').toDate());
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// console.log((data as DateCellDataPB).date);
|
|
||||||
// setSelectedDate(new Date((data as DateCellDataPB).date));
|
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
const onChange = (v: Date | null | (Date | null)[]) => {
|
const onChange = async (v: Date | null | (Date | null)[]) => {
|
||||||
if (v instanceof Date) {
|
if (v instanceof Date) {
|
||||||
console.log(dayjs(v).format('YYYY-MM-DD'));
|
|
||||||
setSelectedDate(v);
|
setSelectedDate(v);
|
||||||
// void cellController?.saveCellData(new DateCellDataPB({ date: dayjs(v).format('YYYY-MM-DD') }));
|
const date = new CalendarData(dayjs(v).add(dayjs().utcOffset(), 'minutes').toDate(), false);
|
||||||
|
await cellController?.saveCellData(date);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<PopupWindow className={'p-2 text-xs'} onOutsideClick={onOutsideClick} left={left} top={top}>
|
||||||
ref={ref}
|
|
||||||
className={`fixed z-10 rounded-lg bg-white px-2 py-2 text-xs shadow-md transition-opacity duration-300 ${
|
|
||||||
adjustedTop === -100 ? 'opacity-0' : 'opacity-100'
|
|
||||||
}`}
|
|
||||||
style={{ top: `${adjustedTop + 40}px`, left: `${left}px` }}
|
|
||||||
>
|
|
||||||
<div className={'px-2'}>
|
<div className={'px-2'}>
|
||||||
<Calendar onChange={(d) => onChange(d)} value={selectedDate} />
|
<Calendar onChange={(d) => onChange(d)} value={selectedDate} />
|
||||||
</div>
|
</div>
|
||||||
@ -92,6 +74,6 @@ export const DatePickerPopup = ({
|
|||||||
<MoreSvg></MoreSvg>
|
<MoreSvg></MoreSvg>
|
||||||
</i>
|
</i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</PopupWindow>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -17,7 +17,7 @@ export const EditCellDate = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} onClick={() => onClick()} className={'px-4 py-2'}>
|
<div ref={ref} onClick={() => onClick()} className={'w-full px-4 py-2'}>
|
||||||
{data?.date || <> </>}
|
{data?.date || <> </>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,195 @@
|
|||||||
|
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
|
||||||
|
import { KeyboardEventHandler, useEffect, useRef, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { SelectOptionColorPB, SelectOptionPB } from '@/services/backend';
|
||||||
|
import { getBgColor } from '$app/components/_shared/getColor';
|
||||||
|
import { SelectOptionCellBackendService } from '$app/stores/effects/database/cell/select_option_bd_svc';
|
||||||
|
import { TrashSvg } from '$app/components/_shared/svg/TrashSvg';
|
||||||
|
import { CheckmarkSvg } from '$app/components/_shared/svg/CheckmarkSvg';
|
||||||
|
import { PopupWindow } from '$app/components/_shared/PopupWindow';
|
||||||
|
|
||||||
|
export const EditCellOptionPopup = ({
|
||||||
|
left,
|
||||||
|
top,
|
||||||
|
cellIdentifier,
|
||||||
|
editingSelectOption,
|
||||||
|
onOutsideClick,
|
||||||
|
}: {
|
||||||
|
left: number;
|
||||||
|
top: number;
|
||||||
|
cellIdentifier: CellIdentifier;
|
||||||
|
editingSelectOption: SelectOptionPB;
|
||||||
|
onOutsideClick: () => void;
|
||||||
|
}) => {
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const { t } = useTranslation('');
|
||||||
|
const [value, setValue] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setValue(editingSelectOption.name);
|
||||||
|
}, [editingSelectOption]);
|
||||||
|
|
||||||
|
const onKeyDown: KeyboardEventHandler = async (e) => {
|
||||||
|
if (e.key === 'Enter' && value.length > 0) {
|
||||||
|
await new SelectOptionCellBackendService(cellIdentifier).createOption({ name: value });
|
||||||
|
setValue('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onKeyDownWrapper: KeyboardEventHandler = (e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
onOutsideClick();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBlur = async () => {
|
||||||
|
const svc = new SelectOptionCellBackendService(cellIdentifier);
|
||||||
|
await svc.updateOption(
|
||||||
|
new SelectOptionPB({
|
||||||
|
id: editingSelectOption.id,
|
||||||
|
color: editingSelectOption.color,
|
||||||
|
name: value,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onColorClick = async (color: SelectOptionColorPB) => {
|
||||||
|
const svc = new SelectOptionCellBackendService(cellIdentifier);
|
||||||
|
await svc.updateOption(
|
||||||
|
new SelectOptionPB({
|
||||||
|
id: editingSelectOption.id,
|
||||||
|
color,
|
||||||
|
name: editingSelectOption.name,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDeleteOptionClick = async () => {
|
||||||
|
const svc = new SelectOptionCellBackendService(cellIdentifier);
|
||||||
|
await svc.deleteOption([editingSelectOption]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PopupWindow
|
||||||
|
className={'p-2 text-xs'}
|
||||||
|
onOutsideClick={async () => {
|
||||||
|
await onBlur();
|
||||||
|
onOutsideClick();
|
||||||
|
}}
|
||||||
|
left={left}
|
||||||
|
top={top}
|
||||||
|
>
|
||||||
|
<div onKeyDown={onKeyDownWrapper} className={'flex flex-col gap-2 p-2'}>
|
||||||
|
<div className={'border-shades-3 flex flex-1 items-center gap-2 rounded border bg-main-selector px-2 '}>
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
className={'py-2'}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
onBlur={() => onBlur()}
|
||||||
|
/>
|
||||||
|
<div className={'font-mono text-shade-3'}>{value.length}/30</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => onDeleteOptionClick()}
|
||||||
|
className={
|
||||||
|
'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 text-main-alert hover:bg-main-secondary'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<i className={'h-5 w-5'}>
|
||||||
|
<TrashSvg></TrashSvg>
|
||||||
|
</i>
|
||||||
|
<span>{t('grid.selectOption.deleteTag')}</span>
|
||||||
|
</button>
|
||||||
|
<div className={'-mx-4 h-[1px] bg-shade-6'}></div>
|
||||||
|
<div className={'my-2 font-medium text-shade-3'}>{t('grid.selectOption.colorPanelTitle')}</div>
|
||||||
|
<div className={'flex flex-col'}>
|
||||||
|
<ColorItem
|
||||||
|
title={t('grid.selectOption.purpleColor')}
|
||||||
|
onClick={() => onColorClick(SelectOptionColorPB.Purple)}
|
||||||
|
bgColor={getBgColor(SelectOptionColorPB.Purple)}
|
||||||
|
checked={editingSelectOption.color === SelectOptionColorPB.Purple}
|
||||||
|
></ColorItem>
|
||||||
|
<ColorItem
|
||||||
|
title={t('grid.selectOption.pinkColor')}
|
||||||
|
onClick={() => onColorClick(SelectOptionColorPB.Pink)}
|
||||||
|
bgColor={getBgColor(SelectOptionColorPB.Pink)}
|
||||||
|
checked={editingSelectOption.color === SelectOptionColorPB.Pink}
|
||||||
|
></ColorItem>
|
||||||
|
<ColorItem
|
||||||
|
title={t('grid.selectOption.lightPinkColor')}
|
||||||
|
onClick={() => onColorClick(SelectOptionColorPB.LightPink)}
|
||||||
|
bgColor={getBgColor(SelectOptionColorPB.LightPink)}
|
||||||
|
checked={editingSelectOption.color === SelectOptionColorPB.LightPink}
|
||||||
|
></ColorItem>
|
||||||
|
<ColorItem
|
||||||
|
title={t('grid.selectOption.orangeColor')}
|
||||||
|
onClick={() => onColorClick(SelectOptionColorPB.Orange)}
|
||||||
|
bgColor={getBgColor(SelectOptionColorPB.Orange)}
|
||||||
|
checked={editingSelectOption.color === SelectOptionColorPB.Orange}
|
||||||
|
></ColorItem>
|
||||||
|
<ColorItem
|
||||||
|
title={t('grid.selectOption.yellowColor')}
|
||||||
|
onClick={() => onColorClick(SelectOptionColorPB.Yellow)}
|
||||||
|
bgColor={getBgColor(SelectOptionColorPB.Yellow)}
|
||||||
|
checked={editingSelectOption.color === SelectOptionColorPB.Yellow}
|
||||||
|
></ColorItem>
|
||||||
|
<ColorItem
|
||||||
|
title={t('grid.selectOption.limeColor')}
|
||||||
|
onClick={() => onColorClick(SelectOptionColorPB.Lime)}
|
||||||
|
bgColor={getBgColor(SelectOptionColorPB.Lime)}
|
||||||
|
checked={editingSelectOption.color === SelectOptionColorPB.Lime}
|
||||||
|
></ColorItem>
|
||||||
|
<ColorItem
|
||||||
|
title={t('grid.selectOption.greenColor')}
|
||||||
|
onClick={() => onColorClick(SelectOptionColorPB.Green)}
|
||||||
|
bgColor={getBgColor(SelectOptionColorPB.Green)}
|
||||||
|
checked={editingSelectOption.color === SelectOptionColorPB.Green}
|
||||||
|
></ColorItem>
|
||||||
|
<ColorItem
|
||||||
|
title={t('grid.selectOption.aquaColor')}
|
||||||
|
onClick={() => onColorClick(SelectOptionColorPB.Aqua)}
|
||||||
|
bgColor={getBgColor(SelectOptionColorPB.Aqua)}
|
||||||
|
checked={editingSelectOption.color === SelectOptionColorPB.Aqua}
|
||||||
|
></ColorItem>
|
||||||
|
<ColorItem
|
||||||
|
title={t('grid.selectOption.blueColor')}
|
||||||
|
onClick={() => onColorClick(SelectOptionColorPB.Blue)}
|
||||||
|
bgColor={getBgColor(SelectOptionColorPB.Blue)}
|
||||||
|
checked={editingSelectOption.color === SelectOptionColorPB.Blue}
|
||||||
|
></ColorItem>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopupWindow>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ColorItem = ({
|
||||||
|
title,
|
||||||
|
bgColor,
|
||||||
|
onClick,
|
||||||
|
checked,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
bgColor: string;
|
||||||
|
onClick: () => void;
|
||||||
|
checked: boolean;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={'flex cursor-pointer items-center justify-between rounded-lg p-2 hover:bg-main-secondary'}
|
||||||
|
onClick={() => onClick()}
|
||||||
|
>
|
||||||
|
<div className={'flex items-center gap-2'}>
|
||||||
|
<div className={`h-4 w-4 rounded-full ${bgColor}`}></div>
|
||||||
|
<span>{title}</span>
|
||||||
|
</div>
|
||||||
|
{checked && (
|
||||||
|
<i className={'block h-3 w-3'}>
|
||||||
|
<CheckmarkSvg></CheckmarkSvg>
|
||||||
|
</i>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -27,9 +27,9 @@ export const EditCellWrapper = ({
|
|||||||
cellIdentifier: CellIdentifier;
|
cellIdentifier: CellIdentifier;
|
||||||
cellCache: CellCache;
|
cellCache: CellCache;
|
||||||
fieldController: FieldController;
|
fieldController: FieldController;
|
||||||
onEditFieldClick: (top: number, right: number) => void;
|
onEditFieldClick: (cell: CellIdentifier, left: number, top: number) => void;
|
||||||
onEditOptionsClick: (left: number, top: number) => void;
|
onEditOptionsClick: (cell: CellIdentifier, left: number, top: number) => void;
|
||||||
onEditDateClick: (left: number, top: number) => void;
|
onEditDateClick: (cell: CellIdentifier, left: number, top: number) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { data, cellController } = useCell(cellIdentifier, cellCache, fieldController);
|
const { data, cellController } = useCell(cellIdentifier, cellCache, fieldController);
|
||||||
const databaseStore = useAppSelector((state) => state.database);
|
const databaseStore = useAppSelector((state) => state.database);
|
||||||
@ -38,7 +38,7 @@ export const EditCellWrapper = ({
|
|||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
if (!el.current) return;
|
if (!el.current) return;
|
||||||
const { top, right } = el.current.getBoundingClientRect();
|
const { top, right } = el.current.getBoundingClientRect();
|
||||||
onEditFieldClick(top, right);
|
onEditFieldClick(cellIdentifier, right, top);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -70,17 +70,23 @@ export const EditCellWrapper = ({
|
|||||||
cellIdentifier.fieldType === FieldType.Checklist) &&
|
cellIdentifier.fieldType === FieldType.Checklist) &&
|
||||||
cellController && (
|
cellController && (
|
||||||
<CellOptions
|
<CellOptions
|
||||||
data={data as SelectOptionCellDataPB | undefined}
|
data={data as SelectOptionCellDataPB}
|
||||||
onEditClick={onEditOptionsClick}
|
onEditClick={(left, top) => onEditOptionsClick(cellIdentifier, left, top)}
|
||||||
></CellOptions>
|
></CellOptions>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{cellIdentifier.fieldType === FieldType.Checkbox && cellController && (
|
{cellIdentifier.fieldType === FieldType.Checkbox && cellController && (
|
||||||
<EditCheckboxCell data={data as boolean | undefined} cellController={cellController}></EditCheckboxCell>
|
<EditCheckboxCell
|
||||||
|
data={data as 'Yes' | 'No' | undefined}
|
||||||
|
cellController={cellController}
|
||||||
|
></EditCheckboxCell>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{cellIdentifier.fieldType === FieldType.DateTime && (
|
{cellIdentifier.fieldType === FieldType.DateTime && (
|
||||||
<EditCellDate data={data as DateCellDataPB | undefined} onEditClick={onEditDateClick}></EditCellDate>
|
<EditCellDate
|
||||||
|
data={data as DateCellDataPB}
|
||||||
|
onEditClick={(left, top) => onEditDateClick(cellIdentifier, left, top)}
|
||||||
|
></EditCellDate>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{cellIdentifier.fieldType === FieldType.Number && cellController && (
|
{cellIdentifier.fieldType === FieldType.Number && cellController && (
|
||||||
@ -88,7 +94,7 @@ export const EditCellWrapper = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{cellIdentifier.fieldType === FieldType.URL && cellController && (
|
{cellIdentifier.fieldType === FieldType.URL && cellController && (
|
||||||
<EditCellUrl data={data as URLCellDataPB | undefined} cellController={cellController}></EditCellUrl>
|
<EditCellUrl data={data as URLCellDataPB} cellController={cellController}></EditCellUrl>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{cellIdentifier.fieldType === FieldType.RichText && cellController && (
|
{cellIdentifier.fieldType === FieldType.RichText && cellController && (
|
||||||
|
@ -1,22 +1,26 @@
|
|||||||
import { EditorCheckSvg } from '$app/components/_shared/svg/EditorCheckSvg';
|
import { EditorCheckSvg } from '$app/components/_shared/svg/EditorCheckSvg';
|
||||||
import { EditorUncheckSvg } from '$app/components/_shared/svg/EditorUncheckSvg';
|
import { EditorUncheckSvg } from '$app/components/_shared/svg/EditorUncheckSvg';
|
||||||
import { CellController } from '$app/stores/effects/database/cell/cell_controller';
|
import { CheckboxCellController } from '$app/stores/effects/database/cell/controller_builder';
|
||||||
|
|
||||||
export const EditCheckboxCell = ({
|
export const EditCheckboxCell = ({
|
||||||
data,
|
data,
|
||||||
cellController,
|
cellController,
|
||||||
}: {
|
}: {
|
||||||
data: boolean | undefined;
|
data: 'Yes' | 'No' | undefined;
|
||||||
cellController: CellController<any, any>;
|
cellController: CheckboxCellController;
|
||||||
}) => {
|
}) => {
|
||||||
const toggleValue = async () => {
|
const toggleValue = async () => {
|
||||||
await cellController?.saveCellData(!data);
|
if (data === 'Yes') {
|
||||||
|
await cellController?.saveCellData('No');
|
||||||
|
} else {
|
||||||
|
await cellController?.saveCellData('Yes');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div onClick={() => toggleValue()} className={'block px-4 py-2'}>
|
<div onClick={() => toggleValue()} className={'block px-4 py-2'}>
|
||||||
<button className={'h-5 w-5'}>
|
<button className={'h-5 w-5'}>
|
||||||
{data ? <EditorCheckSvg></EditorCheckSvg> : <EditorUncheckSvg></EditorUncheckSvg>}
|
{data === 'Yes' ? <EditorCheckSvg></EditorCheckSvg> : <EditorUncheckSvg></EditorUncheckSvg>}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import useOutsideClick from '$app/components/_shared/useOutsideClick';
|
|
||||||
import { TrashSvg } from '$app/components/_shared/svg/TrashSvg';
|
import { TrashSvg } from '$app/components/_shared/svg/TrashSvg';
|
||||||
import { FieldTypeIcon } from '$app/components/_shared/EditRow/FieldTypeIcon';
|
import { FieldTypeIcon } from '$app/components/_shared/EditRow/FieldTypeIcon';
|
||||||
import { FieldTypeName } from '$app/components/_shared/EditRow/FieldTypeName';
|
import { FieldTypeName } from '$app/components/_shared/EditRow/FieldTypeName';
|
||||||
@ -10,10 +9,11 @@ import { FieldInfo } from '$app/stores/effects/database/field/field_controller';
|
|||||||
import { MoreSvg } from '$app/components/_shared/svg/MoreSvg';
|
import { MoreSvg } from '$app/components/_shared/svg/MoreSvg';
|
||||||
import { useAppSelector } from '$app/stores/store';
|
import { useAppSelector } from '$app/stores/store';
|
||||||
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
|
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
|
||||||
|
import { PopupWindow } from '$app/components/_shared/PopupWindow';
|
||||||
|
|
||||||
export const EditFieldPopup = ({
|
export const EditFieldPopup = ({
|
||||||
top,
|
top,
|
||||||
right,
|
left,
|
||||||
cellIdentifier,
|
cellIdentifier,
|
||||||
viewId,
|
viewId,
|
||||||
onOutsideClick,
|
onOutsideClick,
|
||||||
@ -21,7 +21,7 @@ export const EditFieldPopup = ({
|
|||||||
changeFieldTypeClick,
|
changeFieldTypeClick,
|
||||||
}: {
|
}: {
|
||||||
top: number;
|
top: number;
|
||||||
right: number;
|
left: number;
|
||||||
cellIdentifier: CellIdentifier;
|
cellIdentifier: CellIdentifier;
|
||||||
viewId: string;
|
viewId: string;
|
||||||
onOutsideClick: () => void;
|
onOutsideClick: () => void;
|
||||||
@ -30,31 +30,13 @@ export const EditFieldPopup = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const databaseStore = useAppSelector((state) => state.database);
|
const databaseStore = useAppSelector((state) => state.database);
|
||||||
const { t } = useTranslation('');
|
const { t } = useTranslation('');
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
|
||||||
const changeTypeButtonRef = useRef<HTMLDivElement>(null);
|
const changeTypeButtonRef = useRef<HTMLDivElement>(null);
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
|
|
||||||
const [adjustedTop, setAdjustedTop] = useState(-100);
|
|
||||||
|
|
||||||
useOutsideClick(ref, async () => {
|
|
||||||
await save();
|
|
||||||
onOutsideClick();
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setName(databaseStore.fields[cellIdentifier.fieldId].title);
|
setName(databaseStore.fields[cellIdentifier.fieldId].title);
|
||||||
}, [databaseStore, cellIdentifier]);
|
}, [databaseStore, cellIdentifier]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!ref.current) return;
|
|
||||||
const { height } = ref.current.getBoundingClientRect();
|
|
||||||
if (top + height > window.innerHeight) {
|
|
||||||
setAdjustedTop(window.innerHeight - height);
|
|
||||||
} else {
|
|
||||||
setAdjustedTop(top);
|
|
||||||
}
|
|
||||||
}, [ref, window, top, right]);
|
|
||||||
|
|
||||||
const save = async () => {
|
const save = async () => {
|
||||||
if (!fieldInfo) return;
|
if (!fieldInfo) return;
|
||||||
const controller = new TypeOptionController(viewId, Some(fieldInfo));
|
const controller = new TypeOptionController(viewId, Some(fieldInfo));
|
||||||
@ -78,12 +60,14 @@ export const EditFieldPopup = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<PopupWindow
|
||||||
ref={ref}
|
className={'px-2 py-2 text-xs'}
|
||||||
className={`fixed z-10 rounded-lg bg-white px-2 py-2 text-xs shadow-md transition-opacity duration-300 ${
|
onOutsideClick={async () => {
|
||||||
adjustedTop === -100 ? 'opacity-0' : 'opacity-100'
|
await save();
|
||||||
}`}
|
onOutsideClick();
|
||||||
style={{ top: `${adjustedTop}px`, left: `${right + 10}px` }}
|
}}
|
||||||
|
left={left}
|
||||||
|
top={top}
|
||||||
>
|
>
|
||||||
<div className={'flex flex-col gap-2 p-2'}>
|
<div className={'flex flex-col gap-2 p-2'}>
|
||||||
<input
|
<input
|
||||||
@ -125,6 +109,6 @@ export const EditFieldPopup = ({
|
|||||||
</i>
|
</i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</PopupWindow>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -11,10 +11,11 @@ import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
|
|||||||
import { ChangeFieldTypePopup } from '$app/components/_shared/EditRow/ChangeFieldTypePopup';
|
import { ChangeFieldTypePopup } from '$app/components/_shared/EditRow/ChangeFieldTypePopup';
|
||||||
import { TypeOptionController } from '$app/stores/effects/database/field/type_option/type_option_controller';
|
import { TypeOptionController } from '$app/stores/effects/database/field/type_option/type_option_controller';
|
||||||
import { Some } from 'ts-results';
|
import { Some } from 'ts-results';
|
||||||
import { FieldType } from '@/services/backend';
|
import { FieldType, SelectOptionPB } from '@/services/backend';
|
||||||
import { CellOptionsPopup } from '$app/components/_shared/EditRow/CellOptionsPopup';
|
import { CellOptionsPopup } from '$app/components/_shared/EditRow/CellOptionsPopup';
|
||||||
import { DatePickerPopup } from '$app/components/_shared/EditRow/DatePickerPopup';
|
import { DatePickerPopup } from '$app/components/_shared/EditRow/DatePickerPopup';
|
||||||
import { DragDropContext, Droppable, OnDragEndResponder } from 'react-beautiful-dnd';
|
import { DragDropContext, Droppable, OnDragEndResponder } from 'react-beautiful-dnd';
|
||||||
|
import { EditCellOptionPopup } from '$app/components/_shared/EditRow/EditCellOptionPopup';
|
||||||
|
|
||||||
export const EditRow = ({
|
export const EditRow = ({
|
||||||
onClose,
|
onClose,
|
||||||
@ -34,11 +35,11 @@ export const EditRow = ({
|
|||||||
const [editingCell, setEditingCell] = useState<CellIdentifier | null>(null);
|
const [editingCell, setEditingCell] = useState<CellIdentifier | null>(null);
|
||||||
const [showFieldEditor, setShowFieldEditor] = useState(false);
|
const [showFieldEditor, setShowFieldEditor] = useState(false);
|
||||||
const [editFieldTop, setEditFieldTop] = useState(0);
|
const [editFieldTop, setEditFieldTop] = useState(0);
|
||||||
const [editFieldRight, setEditFieldRight] = useState(0);
|
const [editFieldLeft, setEditFieldLeft] = useState(0);
|
||||||
|
|
||||||
const [showChangeFieldTypePopup, setShowChangeFieldTypePopup] = useState(false);
|
const [showChangeFieldTypePopup, setShowChangeFieldTypePopup] = useState(false);
|
||||||
const [changeFieldTypeTop, setChangeFieldTypeTop] = useState(0);
|
const [changeFieldTypeTop, setChangeFieldTypeTop] = useState(0);
|
||||||
const [changeFieldTypeRight, setChangeFieldTypeRight] = useState(0);
|
const [changeFieldTypeLeft, setChangeFieldTypeLeft] = useState(0);
|
||||||
|
|
||||||
const [showChangeOptionsPopup, setShowChangeOptionsPopup] = useState(false);
|
const [showChangeOptionsPopup, setShowChangeOptionsPopup] = useState(false);
|
||||||
const [changeOptionsTop, setChangeOptionsTop] = useState(0);
|
const [changeOptionsTop, setChangeOptionsTop] = useState(0);
|
||||||
@ -48,6 +49,12 @@ export const EditRow = ({
|
|||||||
const [datePickerTop, setDatePickerTop] = useState(0);
|
const [datePickerTop, setDatePickerTop] = useState(0);
|
||||||
const [datePickerLeft, setDatePickerLeft] = useState(0);
|
const [datePickerLeft, setDatePickerLeft] = useState(0);
|
||||||
|
|
||||||
|
const [showEditCellOption, setShowEditCellOption] = useState(false);
|
||||||
|
const [editCellOptionTop, setEditCellOptionTop] = useState(0);
|
||||||
|
const [editCellOptionLeft, setEditCellOptionLeft] = useState(0);
|
||||||
|
|
||||||
|
const [editingSelectOption, setEditingSelectOption] = useState<SelectOptionPB | undefined>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setUnveil(true);
|
setUnveil(true);
|
||||||
}, []);
|
}, []);
|
||||||
@ -59,10 +66,10 @@ export const EditRow = ({
|
|||||||
}, 300);
|
}, 300);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onEditFieldClick = (cellIdentifier: CellIdentifier, top: number, right: number) => {
|
const onEditFieldClick = (cellIdentifier: CellIdentifier, left: number, top: number) => {
|
||||||
setEditingCell(cellIdentifier);
|
setEditingCell(cellIdentifier);
|
||||||
setEditFieldTop(top);
|
setEditFieldTop(top);
|
||||||
setEditFieldRight(right);
|
setEditFieldLeft(left + 10);
|
||||||
setShowFieldEditor(true);
|
setShowFieldEditor(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -74,7 +81,7 @@ export const EditRow = ({
|
|||||||
|
|
||||||
const onChangeFieldTypeClick = (buttonTop: number, buttonRight: number) => {
|
const onChangeFieldTypeClick = (buttonTop: number, buttonRight: number) => {
|
||||||
setChangeFieldTypeTop(buttonTop);
|
setChangeFieldTypeTop(buttonTop);
|
||||||
setChangeFieldTypeRight(buttonRight);
|
setChangeFieldTypeLeft(buttonRight + 30);
|
||||||
setShowChangeFieldTypePopup(true);
|
setShowChangeFieldTypePopup(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -95,17 +102,24 @@ export const EditRow = ({
|
|||||||
const onEditOptionsClick = async (cellIdentifier: CellIdentifier, left: number, top: number) => {
|
const onEditOptionsClick = async (cellIdentifier: CellIdentifier, left: number, top: number) => {
|
||||||
setEditingCell(cellIdentifier);
|
setEditingCell(cellIdentifier);
|
||||||
setChangeOptionsLeft(left);
|
setChangeOptionsLeft(left);
|
||||||
setChangeOptionsTop(top);
|
setChangeOptionsTop(top + 40);
|
||||||
setShowChangeOptionsPopup(true);
|
setShowChangeOptionsPopup(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onEditDateClick = async (cellIdentifier: CellIdentifier, left: number, top: number) => {
|
const onEditDateClick = async (cellIdentifier: CellIdentifier, left: number, top: number) => {
|
||||||
setEditingCell(cellIdentifier);
|
setEditingCell(cellIdentifier);
|
||||||
setDatePickerLeft(left);
|
setDatePickerLeft(left);
|
||||||
setDatePickerTop(top);
|
setDatePickerTop(top + 40);
|
||||||
setShowDatePicker(true);
|
setShowDatePicker(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onOpenOptionDetailClick = (_left: number, _top: number, _select_option: SelectOptionPB) => {
|
||||||
|
setEditingSelectOption(_select_option);
|
||||||
|
setShowEditCellOption(true);
|
||||||
|
setEditCellOptionLeft(_left);
|
||||||
|
setEditCellOptionTop(_top);
|
||||||
|
};
|
||||||
|
|
||||||
const onDragEnd: OnDragEndResponder = (result) => {
|
const onDragEnd: OnDragEndResponder = (result) => {
|
||||||
if (!result.destination?.index) return;
|
if (!result.destination?.index) return;
|
||||||
void controller.moveField({
|
void controller.moveField({
|
||||||
@ -120,8 +134,14 @@ export const EditRow = ({
|
|||||||
className={`fixed inset-0 z-10 flex items-center justify-center bg-black/30 backdrop-blur-sm transition-opacity duration-300 ${
|
className={`fixed inset-0 z-10 flex items-center justify-center bg-black/30 backdrop-blur-sm transition-opacity duration-300 ${
|
||||||
unveil ? 'opacity-100' : 'opacity-0'
|
unveil ? 'opacity-100' : 'opacity-0'
|
||||||
}`}
|
}`}
|
||||||
|
onClick={() => onCloseClick()}
|
||||||
>
|
>
|
||||||
<div className={`relative flex h-[90%] w-[70%] flex-col gap-8 rounded-xl bg-white px-8 pb-4 pt-12`}>
|
<div
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
className={`relative flex h-[90%] w-[70%] flex-col gap-8 rounded-xl bg-white px-8 pb-4 pt-12`}
|
||||||
|
>
|
||||||
<div onClick={() => onCloseClick()} className={'absolute top-4 right-4'}>
|
<div onClick={() => onCloseClick()} className={'absolute top-4 right-4'}>
|
||||||
<button className={'block h-8 w-8 rounded-lg text-shade-2 hover:bg-main-secondary'}>
|
<button className={'block h-8 w-8 rounded-lg text-shade-2 hover:bg-main-secondary'}>
|
||||||
<CloseSvg></CloseSvg>
|
<CloseSvg></CloseSvg>
|
||||||
@ -145,11 +165,9 @@ export const EditRow = ({
|
|||||||
cellIdentifier={cell.cellIdentifier}
|
cellIdentifier={cell.cellIdentifier}
|
||||||
cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
|
cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
|
||||||
fieldController={controller.fieldController}
|
fieldController={controller.fieldController}
|
||||||
onEditFieldClick={(top: number, right: number) => onEditFieldClick(cell.cellIdentifier, top, right)}
|
onEditFieldClick={onEditFieldClick}
|
||||||
onEditOptionsClick={(left: number, top: number) =>
|
onEditOptionsClick={onEditOptionsClick}
|
||||||
onEditOptionsClick(cell.cellIdentifier, left, top)
|
onEditDateClick={onEditDateClick}
|
||||||
}
|
|
||||||
onEditDateClick={(left: number, top: number) => onEditDateClick(cell.cellIdentifier, left, top)}
|
|
||||||
></EditCellWrapper>
|
></EditCellWrapper>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -172,7 +190,7 @@ export const EditRow = ({
|
|||||||
{showFieldEditor && editingCell && (
|
{showFieldEditor && editingCell && (
|
||||||
<EditFieldPopup
|
<EditFieldPopup
|
||||||
top={editFieldTop}
|
top={editFieldTop}
|
||||||
right={editFieldRight}
|
left={editFieldLeft}
|
||||||
cellIdentifier={editingCell}
|
cellIdentifier={editingCell}
|
||||||
viewId={viewId}
|
viewId={viewId}
|
||||||
onOutsideClick={onOutsideEditFieldClick}
|
onOutsideClick={onOutsideEditFieldClick}
|
||||||
@ -183,7 +201,7 @@ export const EditRow = ({
|
|||||||
{showChangeFieldTypePopup && (
|
{showChangeFieldTypePopup && (
|
||||||
<ChangeFieldTypePopup
|
<ChangeFieldTypePopup
|
||||||
top={changeFieldTypeTop}
|
top={changeFieldTypeTop}
|
||||||
right={changeFieldTypeRight}
|
left={changeFieldTypeLeft}
|
||||||
onClick={(newType) => changeFieldType(newType)}
|
onClick={(newType) => changeFieldType(newType)}
|
||||||
onOutsideClick={() => setShowChangeFieldTypePopup(false)}
|
onOutsideClick={() => setShowChangeFieldTypePopup(false)}
|
||||||
></ChangeFieldTypePopup>
|
></ChangeFieldTypePopup>
|
||||||
@ -196,6 +214,7 @@ export const EditRow = ({
|
|||||||
cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
|
cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
|
||||||
fieldController={controller.fieldController}
|
fieldController={controller.fieldController}
|
||||||
onOutsideClick={() => setShowChangeOptionsPopup(false)}
|
onOutsideClick={() => setShowChangeOptionsPopup(false)}
|
||||||
|
openOptionDetail={onOpenOptionDetailClick}
|
||||||
></CellOptionsPopup>
|
></CellOptionsPopup>
|
||||||
)}
|
)}
|
||||||
{showDatePicker && editingCell && (
|
{showDatePicker && editingCell && (
|
||||||
@ -208,6 +227,17 @@ export const EditRow = ({
|
|||||||
onOutsideClick={() => setShowDatePicker(false)}
|
onOutsideClick={() => setShowDatePicker(false)}
|
||||||
></DatePickerPopup>
|
></DatePickerPopup>
|
||||||
)}
|
)}
|
||||||
|
{showEditCellOption && editingCell && editingSelectOption && (
|
||||||
|
<EditCellOptionPopup
|
||||||
|
top={editCellOptionTop}
|
||||||
|
left={editCellOptionLeft}
|
||||||
|
cellIdentifier={editingCell}
|
||||||
|
editingSelectOption={editingSelectOption}
|
||||||
|
onOutsideClick={() => {
|
||||||
|
setShowEditCellOption(false);
|
||||||
|
}}
|
||||||
|
></EditCellOptionPopup>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { IPopupItem, Popup } from './Popup';
|
import { IPopupItem, PopupSelect } from './PopupSelect';
|
||||||
import i18n from 'i18next';
|
import i18n from 'i18next';
|
||||||
|
|
||||||
const supportedLanguages: { key: string; title: string }[] = [
|
const supportedLanguages: { key: string; title: string }[] = [
|
||||||
@ -37,11 +37,11 @@ export const LanguageSelectPopup = ({ onClose }: { onClose: () => void }) => {
|
|||||||
icon: <></>,
|
icon: <></>,
|
||||||
}));
|
}));
|
||||||
return (
|
return (
|
||||||
<Popup
|
<PopupSelect
|
||||||
items={items}
|
items={items}
|
||||||
className={'absolute top-full right-0 z-10 w-[200px]'}
|
className={'absolute top-full right-0 z-10 w-[200px]'}
|
||||||
onOutsideClick={onClose}
|
onOutsideClick={onClose}
|
||||||
columns={2}
|
columns={2}
|
||||||
></Popup>
|
></PopupSelect>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -2,12 +2,12 @@ import { MouseEvent, ReactNode, useRef } from 'react';
|
|||||||
import useOutsideClick from './useOutsideClick';
|
import useOutsideClick from './useOutsideClick';
|
||||||
|
|
||||||
export interface IPopupItem {
|
export interface IPopupItem {
|
||||||
icon: ReactNode;
|
icon: ReactNode | (() => JSX.Element);
|
||||||
title: string;
|
title: string;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Popup = ({
|
export const PopupSelect = ({
|
||||||
items,
|
items,
|
||||||
className = '',
|
className = '',
|
||||||
onOutsideClick,
|
onOutsideClick,
|
||||||
@ -31,18 +31,20 @@ export const Popup = ({
|
|||||||
return (
|
return (
|
||||||
<div ref={ref} className={`${className} rounded-lg bg-white px-2 py-2 shadow-md`} style={style}>
|
<div ref={ref} className={`${className} rounded-lg bg-white px-2 py-2 shadow-md`} style={style}>
|
||||||
<div
|
<div
|
||||||
className={`grid ${columns === 1 && 'grid-cols-1'} ${columns === 2 && 'grid-cols-2'} ${
|
className={
|
||||||
columns === 3 && 'grid-cols-3'
|
(columns === 2 ? 'grid grid-cols-2' : '') + (columns === 3 ? 'grid grid-cols-3' : '') + ' w-full gap-x-4'
|
||||||
} gap-x-4`}
|
}
|
||||||
>
|
>
|
||||||
{items.map((item, index) => (
|
{items.map((item, index) => (
|
||||||
<button
|
<button
|
||||||
key={index}
|
key={index}
|
||||||
className={'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 hover:bg-main-secondary'}
|
className={'flex w-full cursor-pointer items-center gap-2 rounded-lg px-2 py-2 hover:bg-main-secondary'}
|
||||||
onClick={(e) => handleClick(e, item)}
|
onClick={(e) => handleClick(e, item)}
|
||||||
>
|
>
|
||||||
{item.icon}
|
<>
|
||||||
<span className={'flex-shrink-0'}>{item.title}</span>
|
{typeof item.icon === 'function' ? item.icon() : item.icon}
|
||||||
|
<span className={'flex-shrink-0'}>{item.title}</span>
|
||||||
|
</>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
@ -0,0 +1,51 @@
|
|||||||
|
import { ReactNode, useEffect, useRef, useState } from 'react';
|
||||||
|
import useOutsideClick from '$app/components/_shared/useOutsideClick';
|
||||||
|
|
||||||
|
export const PopupWindow = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
onOutsideClick,
|
||||||
|
left,
|
||||||
|
top,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
className: string;
|
||||||
|
onOutsideClick: () => void;
|
||||||
|
left: number;
|
||||||
|
top: number;
|
||||||
|
}) => {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
useOutsideClick(ref, onOutsideClick);
|
||||||
|
|
||||||
|
const [adjustedTop, setAdjustedTop] = useState(-100);
|
||||||
|
const [adjustedLeft, setAdjustedLeft] = useState(-100);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ref.current) return;
|
||||||
|
const { height, width } = ref.current.getBoundingClientRect();
|
||||||
|
if (top + height > window.innerHeight) {
|
||||||
|
setAdjustedTop(window.innerHeight - height);
|
||||||
|
} else {
|
||||||
|
setAdjustedTop(top);
|
||||||
|
}
|
||||||
|
if (left + width > window.innerWidth) {
|
||||||
|
setAdjustedLeft(window.innerWidth - width);
|
||||||
|
} else {
|
||||||
|
setAdjustedLeft(left);
|
||||||
|
}
|
||||||
|
}, [ref, left, top, window]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={
|
||||||
|
'fixed z-10 rounded-lg bg-white shadow-md transition-opacity duration-300 ' +
|
||||||
|
(adjustedTop === -100 && adjustedLeft === -100 ? 'opacity-0 ' : 'opacity-100 ') +
|
||||||
|
(className || '')
|
||||||
|
}
|
||||||
|
style={{ top: `${adjustedTop}px`, left: `${adjustedLeft}px` }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -56,7 +56,15 @@ export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
|
|||||||
void loadFields(fieldInfos);
|
void loadFields(fieldInfos);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await controller.open();
|
|
||||||
|
const openResult = await controller.open();
|
||||||
|
if (openResult.ok) {
|
||||||
|
setRows(
|
||||||
|
openResult.val.map((pb) => {
|
||||||
|
return new RowInfo(viewId, controller.fieldController.fieldInfos, pb);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (type === ViewLayoutPB.Board) {
|
if (type === ViewLayoutPB.Board) {
|
||||||
const fieldId = await controller.getGroupByFieldId();
|
const fieldId = await controller.getGroupByFieldId();
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export const EyeClosed = () => {
|
export const EyeClosedSvg = () => {
|
||||||
return (
|
return (
|
||||||
<svg width='100%' height='100%' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
<svg width='100%' height='100%' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||||
<path
|
<path
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export const EyeOpened = () => {
|
export const EyeOpenSvg = () => {
|
||||||
return (
|
return (
|
||||||
<svg width='100%' height='100%' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
<svg width='100%' height='100%' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||||
<path
|
<path
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
export const FullView = () => {
|
||||||
|
return (
|
||||||
|
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||||
|
<path d='M6 13H3V10' stroke='#333333' strokeLinecap='round' strokeLinejoin='round' />
|
||||||
|
<path d='M10 3H13V6' stroke='#333333' strokeLinecap='round' strokeLinejoin='round' />
|
||||||
|
<path d='M3 13L7 9' stroke='#333333' strokeLinecap='round' strokeLinejoin='round' />
|
||||||
|
<path d='M13 3L9 7' stroke='#333333' strokeLinecap='round' strokeLinejoin='round' />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,26 @@
|
|||||||
|
export const GroupByFieldSvg = () => {
|
||||||
|
return (
|
||||||
|
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||||
|
<path
|
||||||
|
d='M10 2H13C13.5523 2 14 2.44772 14 3V6'
|
||||||
|
stroke='currentColor'
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
/>
|
||||||
|
<path d='M6 2H3C2.44772 2 2 2.44772 2 3V6' stroke='currentColor' strokeLinecap='round' strokeLinejoin='round' />
|
||||||
|
<path
|
||||||
|
d='M6 14H3C2.44772 14 2 13.5523 2 13V10'
|
||||||
|
stroke='currentColor'
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d='M10 14H13C13.5523 14 14 13.5523 14 13V10'
|
||||||
|
stroke='currentColor'
|
||||||
|
strokeLinecap='round'
|
||||||
|
strokeLinejoin='round'
|
||||||
|
/>
|
||||||
|
<rect x='6' y='6' width='4' height='4' rx='1' stroke='currentColor' />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,11 @@
|
|||||||
|
export const GroupBySvg = () => {
|
||||||
|
return (
|
||||||
|
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||||
|
<path d='M10 2H13C13.5523 2 14 2.44772 14 3V6' stroke='#333333' strokeLinecap='round' strokeLinejoin='round' />
|
||||||
|
<path d='M6 2H3C2.44772 2 2 2.44772 2 3V6' stroke='#333333' strokeLinecap='round' strokeLinejoin='round' />
|
||||||
|
<path d='M6 14H3C2.44772 14 2 13.5523 2 13V10' stroke='#333333' strokeLinecap='round' strokeLinejoin='round' />
|
||||||
|
<path d='M10 14H13C13.5523 14 14 13.5523 14 13V10' stroke='#333333' strokeLinecap='round' strokeLinejoin='round' />
|
||||||
|
<rect x='6' y='6' width='4' height='4' rx='1' stroke='#333333' />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
@ -1,6 +1,6 @@
|
|||||||
import { AppflowyLogo } from '../../_shared/svg/AppflowyLogo';
|
import { AppflowyLogo } from '../../_shared/svg/AppflowyLogo';
|
||||||
import { EyeClosed } from '../../_shared/svg/EyeClosedSvg';
|
import { EyeClosedSvg } from '../../_shared/svg/EyeClosedSvg';
|
||||||
import { EyeOpened } from '../../_shared/svg/EyeOpenSvg';
|
import { EyeOpenSvg } from '../../_shared/svg/EyeOpenSvg';
|
||||||
import { useLogin } from './Login.hooks';
|
import { useLogin } from './Login.hooks';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Button } from '../../_shared/Button';
|
import { Button } from '../../_shared/Button';
|
||||||
@ -55,7 +55,7 @@ export const Login = () => {
|
|||||||
className='absolute right-0 top-0 flex h-full w-12 items-center justify-center '
|
className='absolute right-0 top-0 flex h-full w-12 items-center justify-center '
|
||||||
onClick={onTogglePassword}
|
onClick={onTogglePassword}
|
||||||
>
|
>
|
||||||
<span className='h-6 w-6'>{showPassword ? <EyeClosed /> : <EyeOpened />}</span>
|
<span className='h-6 w-6'>{showPassword ? <EyeClosedSvg /> : <EyeOpenSvg />}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { AppflowyLogo } from '../../_shared/svg/AppflowyLogo';
|
import { AppflowyLogo } from '../../_shared/svg/AppflowyLogo';
|
||||||
import { EyeClosed } from '../../_shared/svg/EyeClosedSvg';
|
import { EyeClosedSvg } from '../../_shared/svg/EyeClosedSvg';
|
||||||
import { EyeOpened } from '../../_shared/svg/EyeOpenSvg';
|
import { EyeOpenSvg } from '../../_shared/svg/EyeOpenSvg';
|
||||||
|
|
||||||
import { useSignUp } from './SignUp.hooks';
|
import { useSignUp } from './SignUp.hooks';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
@ -71,7 +71,7 @@ export const SignUp = () => {
|
|||||||
onClick={onTogglePassword}
|
onClick={onTogglePassword}
|
||||||
type='button'
|
type='button'
|
||||||
>
|
>
|
||||||
<span className='h-6 w-6'>{showPassword ? <EyeClosed /> : <EyeOpened />}</span>
|
<span className='h-6 w-6'>{showPassword ? <EyeClosedSvg /> : <EyeOpenSvg />}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -89,7 +89,7 @@ export const SignUp = () => {
|
|||||||
onClick={onToggleConfirmPassword}
|
onClick={onToggleConfirmPassword}
|
||||||
type='button'
|
type='button'
|
||||||
>
|
>
|
||||||
<span className='h-6 w-6'>{showConfirmPassword ? <EyeClosed /> : <EyeOpened />}</span>
|
<span className='h-6 w-6'>{showConfirmPassword ? <EyeClosedSvg /> : <EyeOpenSvg />}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { SettingsSvg } from '../_shared/svg/SettingsSvg';
|
|
||||||
import { SearchInput } from '../_shared/SearchInput';
|
import { SearchInput } from '../_shared/SearchInput';
|
||||||
import { BoardBlock } from './BoardBlock';
|
import { BoardGroup } from './BoardGroup';
|
||||||
import { NewBoardBlock } from './NewBoardBlock';
|
import { NewBoardBlock } from './NewBoardBlock';
|
||||||
import { useDatabase } from '../_shared/database-hooks/useDatabase';
|
import { useDatabase } from '../_shared/database-hooks/useDatabase';
|
||||||
import { ViewLayoutPB } from '@/services/backend';
|
import { ViewLayoutPB } from '@/services/backend';
|
||||||
@ -8,8 +7,9 @@ import { DragDropContext } from 'react-beautiful-dnd';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { RowInfo } from '$app/stores/effects/database/row/row_cache';
|
import { RowInfo } from '$app/stores/effects/database/row/row_cache';
|
||||||
import { EditRow } from '$app/components/_shared/EditRow/EditRow';
|
import { EditRow } from '$app/components/_shared/EditRow/EditRow';
|
||||||
|
import { BoardToolbar } from '$app/components/board/BoardToolbar';
|
||||||
|
|
||||||
export const Board = ({ viewId }: { viewId: string }) => {
|
export const Board = ({ viewId, title }: { viewId: string; title: string }) => {
|
||||||
const { controller, rows, groups, groupByFieldId, onNewRowClick, onDragEnd } = useDatabase(viewId, ViewLayoutPB.Board);
|
const { controller, rows, groups, groupByFieldId, onNewRowClick, onDragEnd } = useDatabase(viewId, ViewLayoutPB.Board);
|
||||||
const [showBoardRow, setShowBoardRow] = useState(false);
|
const [showBoardRow, setShowBoardRow] = useState(false);
|
||||||
const [boardRowInfo, setBoardRowInfo] = useState<RowInfo>();
|
const [boardRowInfo, setBoardRowInfo] = useState<RowInfo>();
|
||||||
@ -22,12 +22,7 @@ export const Board = ({ viewId }: { viewId: string }) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='flex w-full items-center justify-between'>
|
<div className='flex w-full items-center justify-between'>
|
||||||
<div className={'flex items-center text-xl font-semibold'}>
|
<BoardToolbar title={title} />
|
||||||
<div>{'Kanban'}</div>
|
|
||||||
<button className={'ml-2 h-5 w-5'}>
|
|
||||||
<SettingsSvg></SettingsSvg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex shrink-0 items-center gap-4'>
|
<div className='flex shrink-0 items-center gap-4'>
|
||||||
<SearchInput />
|
<SearchInput />
|
||||||
@ -39,7 +34,7 @@ export const Board = ({ viewId }: { viewId: string }) => {
|
|||||||
{controller &&
|
{controller &&
|
||||||
groups &&
|
groups &&
|
||||||
groups.map((group, index) => (
|
groups.map((group, index) => (
|
||||||
<BoardBlock
|
<BoardGroup
|
||||||
key={group.groupId}
|
key={group.groupId}
|
||||||
viewId={viewId}
|
viewId={viewId}
|
||||||
controller={controller}
|
controller={controller}
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { Details2Svg } from '../_shared/svg/Details2Svg';
|
import { Details2Svg } from '../_shared/svg/Details2Svg';
|
||||||
import { RowInfo } from '../../stores/effects/database/row/row_cache';
|
import { RowInfo } from '$app/stores/effects/database/row/row_cache';
|
||||||
import { useRow } from '../_shared/database-hooks/useRow';
|
import { useRow } from '../_shared/database-hooks/useRow';
|
||||||
import { DatabaseController } from '../../stores/effects/database/database_controller';
|
import { DatabaseController } from '$app/stores/effects/database/database_controller';
|
||||||
import { BoardCell } from './BoardCell';
|
import { BoardCell } from './BoardCell';
|
||||||
import { Draggable } from 'react-beautiful-dnd';
|
import { Draggable } from 'react-beautiful-dnd';
|
||||||
|
import { MouseEventHandler } from 'react';
|
||||||
|
|
||||||
export const BoardCard = ({
|
export const BoardCard = ({
|
||||||
index,
|
index,
|
||||||
@ -22,6 +23,11 @@ export const BoardCard = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { cells } = useRow(viewId, controller, rowInfo);
|
const { cells } = useRow(viewId, controller, rowInfo);
|
||||||
|
|
||||||
|
const onDetailClick: MouseEventHandler = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
// onOpenRow(rowInfo);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Draggable draggableId={rowInfo.row.id} index={index}>
|
<Draggable draggableId={rowInfo.row.id} index={index}>
|
||||||
{(provided) => (
|
{(provided) => (
|
||||||
@ -32,7 +38,7 @@ export const BoardCard = ({
|
|||||||
onClick={() => onOpenRow(rowInfo)}
|
onClick={() => onOpenRow(rowInfo)}
|
||||||
className={`relative cursor-pointer select-none rounded-lg border border-shade-6 bg-white px-3 py-2 transition-transform duration-100 hover:bg-main-selector `}
|
className={`relative cursor-pointer select-none rounded-lg border border-shade-6 bg-white px-3 py-2 transition-transform duration-100 hover:bg-main-selector `}
|
||||||
>
|
>
|
||||||
<button className={'absolute right-4 top-2.5 h-5 w-5 rounded hover:bg-surface-2'}>
|
<button onClick={onDetailClick} className={'absolute right-4 top-2.5 h-5 w-5 rounded hover:bg-surface-2'}>
|
||||||
<Details2Svg></Details2Svg>
|
<Details2Svg></Details2Svg>
|
||||||
</button>
|
</button>
|
||||||
<div className={'flex flex-col gap-3'}>
|
<div className={'flex flex-col gap-3'}>
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { CellIdentifier } from '../../stores/effects/database/cell/cell_bd_svc';
|
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
|
||||||
import { CellCache } from '../../stores/effects/database/cell/cell_cache';
|
import { CellCache } from '$app/stores/effects/database/cell/cell_cache';
|
||||||
import { FieldController } from '../../stores/effects/database/field/field_controller';
|
import { FieldController } from '$app/stores/effects/database/field/field_controller';
|
||||||
import { FieldType } from '../../../services/backend';
|
import { FieldType } from '@/services/backend';
|
||||||
import { BoardOptionsCell } from './BoardOptionsCell';
|
import { BoardOptionsCell } from './BoardOptionsCell';
|
||||||
import { BoardDateCell } from './BoardDateCell';
|
import { BoardDateCell } from './BoardDateCell';
|
||||||
import { BoardTextCell } from './BoardTextCell';
|
import { BoardTextCell } from './BoardTextCell';
|
||||||
import { BoardUrlCell } from '$app/components/board/BoardUrlCell';
|
import { BoardUrlCell } from '$app/components/board/BoardUrlCell';
|
||||||
|
import { BoardCheckboxCell } from '$app/components/board/BoardCheckboxCell';
|
||||||
|
|
||||||
export const BoardCell = ({
|
export const BoardCell = ({
|
||||||
cellIdentifier,
|
cellIdentifier,
|
||||||
@ -38,6 +39,12 @@ export const BoardCell = ({
|
|||||||
cellCache={cellCache}
|
cellCache={cellCache}
|
||||||
fieldController={fieldController}
|
fieldController={fieldController}
|
||||||
></BoardUrlCell>
|
></BoardUrlCell>
|
||||||
|
) : cellIdentifier.fieldType === FieldType.Checkbox ? (
|
||||||
|
<BoardCheckboxCell
|
||||||
|
cellIdentifier={cellIdentifier}
|
||||||
|
cellCache={cellCache}
|
||||||
|
fieldController={fieldController}
|
||||||
|
></BoardCheckboxCell>
|
||||||
) : (
|
) : (
|
||||||
<BoardTextCell
|
<BoardTextCell
|
||||||
cellIdentifier={cellIdentifier}
|
cellIdentifier={cellIdentifier}
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
import { EditorCheckSvg } from '$app/components/_shared/svg/EditorCheckSvg';
|
||||||
|
import { EditorUncheckSvg } from '$app/components/_shared/svg/EditorUncheckSvg';
|
||||||
|
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
|
||||||
|
import { CellCache } from '$app/stores/effects/database/cell/cell_cache';
|
||||||
|
import { FieldController } from '$app/stores/effects/database/field/field_controller';
|
||||||
|
import { useCell } from '$app/components/_shared/database-hooks/useCell';
|
||||||
|
|
||||||
|
export const BoardCheckboxCell = ({
|
||||||
|
cellIdentifier,
|
||||||
|
cellCache,
|
||||||
|
fieldController,
|
||||||
|
}: {
|
||||||
|
cellIdentifier: CellIdentifier;
|
||||||
|
cellCache: CellCache;
|
||||||
|
fieldController: FieldController;
|
||||||
|
}) => {
|
||||||
|
const { data } = useCell(cellIdentifier, cellCache, fieldController);
|
||||||
|
return (
|
||||||
|
<i className={'h-5 w-5'}>
|
||||||
|
{data === 'Yes' ? <EditorCheckSvg></EditorCheckSvg> : <EditorUncheckSvg></EditorUncheckSvg>}
|
||||||
|
</i>
|
||||||
|
);
|
||||||
|
};
|
@ -1,8 +1,8 @@
|
|||||||
import { DateCellDataPB } from '../../../services/backend';
|
import { DateCellDataPB } from '@/services/backend';
|
||||||
import { useCell } from '../_shared/database-hooks/useCell';
|
import { useCell } from '../_shared/database-hooks/useCell';
|
||||||
import { CellIdentifier } from '../../stores/effects/database/cell/cell_bd_svc';
|
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
|
||||||
import { CellCache } from '../../stores/effects/database/cell/cell_cache';
|
import { CellCache } from '$app/stores/effects/database/cell/cell_cache';
|
||||||
import { FieldController } from '../../stores/effects/database/field/field_controller';
|
import { FieldController } from '$app/stores/effects/database/field/field_controller';
|
||||||
|
|
||||||
export const BoardDateCell = ({
|
export const BoardDateCell = ({
|
||||||
cellIdentifier,
|
cellIdentifier,
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
import { useAppSelector } from '$app/stores/store';
|
||||||
|
import { FieldTypeIcon } from '$app/components/_shared/EditRow/FieldTypeIcon';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
import useOutsideClick from '$app/components/_shared/useOutsideClick';
|
||||||
|
import { EyeOpenSvg } from '$app/components/_shared/svg/EyeOpenSvg';
|
||||||
|
|
||||||
|
export const BoardFieldsPopup = ({ hidePopup }: { hidePopup: () => void }) => {
|
||||||
|
const columns = useAppSelector((state) => state.database.columns);
|
||||||
|
const fields = useAppSelector((state) => state.database.fields);
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
useOutsideClick(ref, () => hidePopup());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref} className={'absolute top-full left-full z-10 rounded-lg bg-white px-2 py-2 text-xs shadow-md'}>
|
||||||
|
{columns.map((column, index) => (
|
||||||
|
<div
|
||||||
|
className={'flex cursor-pointer items-center justify-between rounded-lg px-2 py-2 hover:bg-main-secondary'}
|
||||||
|
key={index}
|
||||||
|
>
|
||||||
|
<div className={'flex items-center gap-2 '}>
|
||||||
|
<i className={'flex h-5 w-5 flex-shrink-0 items-center justify-center'}>
|
||||||
|
<FieldTypeIcon fieldType={fields[column.fieldId].fieldType}></FieldTypeIcon>
|
||||||
|
</i>
|
||||||
|
<span className={'flex-shrink-0'}>{fields[column.fieldId].title}</span>
|
||||||
|
</div>
|
||||||
|
<div className={'ml-12'}>
|
||||||
|
<i className={'block h-5 w-5'}>
|
||||||
|
<EyeOpenSvg></EyeOpenSvg>
|
||||||
|
</i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,12 +1,12 @@
|
|||||||
import { Details2Svg } from '../_shared/svg/Details2Svg';
|
import { Details2Svg } from '../_shared/svg/Details2Svg';
|
||||||
import AddSvg from '../_shared/svg/AddSvg';
|
import AddSvg from '../_shared/svg/AddSvg';
|
||||||
import { BoardCard } from './BoardCard';
|
import { BoardCard } from './BoardCard';
|
||||||
import { RowInfo } from '../../stores/effects/database/row/row_cache';
|
import { RowInfo } from '$app/stores/effects/database/row/row_cache';
|
||||||
import { DatabaseController } from '../../stores/effects/database/database_controller';
|
import { DatabaseController } from '$app/stores/effects/database/database_controller';
|
||||||
import { Droppable } from 'react-beautiful-dnd';
|
import { Droppable } from 'react-beautiful-dnd';
|
||||||
import { DatabaseGroupController } from '$app/stores/effects/database/group/group_controller';
|
import { DatabaseGroupController } from '$app/stores/effects/database/group/group_controller';
|
||||||
|
|
||||||
export const BoardBlock = ({
|
export const BoardGroup = ({
|
||||||
viewId,
|
viewId,
|
||||||
controller,
|
controller,
|
||||||
allRows,
|
allRows,
|
@ -0,0 +1,35 @@
|
|||||||
|
import { useAppSelector } from '$app/stores/store';
|
||||||
|
import { FieldTypeIcon } from '$app/components/_shared/EditRow/FieldTypeIcon';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
import useOutsideClick from '$app/components/_shared/useOutsideClick';
|
||||||
|
import { CheckmarkSvg } from '$app/components/_shared/svg/CheckmarkSvg';
|
||||||
|
|
||||||
|
export const BoardGroupFieldsPopup = ({ hidePopup }: { hidePopup: () => void }) => {
|
||||||
|
const columns = useAppSelector((state) => state.database.columns);
|
||||||
|
const fields = useAppSelector((state) => state.database.fields);
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
useOutsideClick(ref, () => hidePopup());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref} className={'absolute top-full left-full z-10 rounded-lg bg-white px-2 py-2 text-xs shadow-md'}>
|
||||||
|
{columns.map((column, index) => (
|
||||||
|
<div
|
||||||
|
className={'flex cursor-pointer items-center justify-between rounded-lg px-2 py-2 hover:bg-main-secondary'}
|
||||||
|
key={index}
|
||||||
|
>
|
||||||
|
<div className={'flex items-center gap-2 '}>
|
||||||
|
<i className={'flex h-5 w-5 flex-shrink-0 items-center justify-center'}>
|
||||||
|
<FieldTypeIcon fieldType={fields[column.fieldId].fieldType}></FieldTypeIcon>
|
||||||
|
</i>
|
||||||
|
<span className={'flex-shrink-0'}>{fields[column.fieldId].title}</span>
|
||||||
|
</div>
|
||||||
|
<div className={'ml-12'}>
|
||||||
|
<i className={'block h-3 w-3'}>
|
||||||
|
<CheckmarkSvg></CheckmarkSvg>
|
||||||
|
</i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,8 +1,8 @@
|
|||||||
import { SelectOptionCellDataPB } from '../../../services/backend';
|
import { SelectOptionCellDataPB } from '@/services/backend';
|
||||||
import { useCell } from '../_shared/database-hooks/useCell';
|
import { useCell } from '../_shared/database-hooks/useCell';
|
||||||
import { CellIdentifier } from '../../stores/effects/database/cell/cell_bd_svc';
|
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
|
||||||
import { CellCache } from '../../stores/effects/database/cell/cell_cache';
|
import { CellCache } from '$app/stores/effects/database/cell/cell_cache';
|
||||||
import { FieldController } from '../../stores/effects/database/field/field_controller';
|
import { FieldController } from '$app/stores/effects/database/field/field_controller';
|
||||||
import { getBgColor } from '$app/components/_shared/getColor';
|
import { getBgColor } from '$app/components/_shared/getColor';
|
||||||
|
|
||||||
export const BoardOptionsCell = ({
|
export const BoardOptionsCell = ({
|
||||||
@ -18,7 +18,7 @@ export const BoardOptionsCell = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex flex-wrap items-center gap-2 py-2 text-xs text-black'}>
|
<div className={'flex flex-wrap items-center gap-2 py-2 text-xs text-black'}>
|
||||||
{(data as SelectOptionCellDataPB | undefined)?.select_options?.map((option, index) => (
|
{(data as SelectOptionCellDataPB)?.select_options?.map((option, index) => (
|
||||||
<div className={`${getBgColor(option.color)} rounded px-2 py-0.5`} key={index}>
|
<div className={`${getBgColor(option.color)} rounded px-2 py-0.5`} key={index}>
|
||||||
{option?.name || ''}
|
{option?.name || ''}
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,48 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { PropertiesSvg } from '$app/components/_shared/svg/PropertiesSvg';
|
||||||
|
import { IPopupItem, PopupSelect } from '$app/components/_shared/PopupSelect';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { GroupByFieldSvg } from '$app/components/_shared/svg/GroupByFieldSvg';
|
||||||
|
|
||||||
|
export const BoardSettingsPopup = ({
|
||||||
|
hidePopup,
|
||||||
|
onFieldsClick,
|
||||||
|
onGroupClick,
|
||||||
|
}: {
|
||||||
|
hidePopup: () => void;
|
||||||
|
onFieldsClick: () => void;
|
||||||
|
onGroupClick: () => void;
|
||||||
|
}) => {
|
||||||
|
const [settingsItems, setSettingsItems] = useState<IPopupItem[]>([]);
|
||||||
|
const { t } = useTranslation('');
|
||||||
|
useEffect(() => {
|
||||||
|
setSettingsItems([
|
||||||
|
{
|
||||||
|
icon: (
|
||||||
|
<i className={'h-5 w-5'}>
|
||||||
|
<PropertiesSvg></PropertiesSvg>
|
||||||
|
</i>
|
||||||
|
),
|
||||||
|
title: t('grid.settings.Properties'),
|
||||||
|
onClick: onFieldsClick,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: (
|
||||||
|
<i className={'h-5 w-5'}>
|
||||||
|
<GroupByFieldSvg></GroupByFieldSvg>
|
||||||
|
</i>
|
||||||
|
),
|
||||||
|
title: t('grid.settings.group'),
|
||||||
|
onClick: onGroupClick,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}, [t]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PopupSelect
|
||||||
|
onOutsideClick={() => hidePopup()}
|
||||||
|
items={settingsItems}
|
||||||
|
className={'absolute top-full left-full z-10 text-xs'}
|
||||||
|
></PopupSelect>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,37 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
export const useBoardToolbar = () => {
|
||||||
|
const [showSettings, setShowSettings] = useState(false);
|
||||||
|
const [showAllFields, setShowAllFields] = useState(false);
|
||||||
|
const [showGroupFields, setShowGroupFields] = useState(false);
|
||||||
|
|
||||||
|
const onSettingsClick = () => {
|
||||||
|
setShowSettings(!showSettings);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFieldsClick = () => {
|
||||||
|
setShowSettings(false);
|
||||||
|
setShowAllFields(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onGroupClick = () => {
|
||||||
|
setShowSettings(false);
|
||||||
|
setShowGroupFields(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hidePopup = () => {
|
||||||
|
setShowSettings(false);
|
||||||
|
setShowAllFields(false);
|
||||||
|
setShowGroupFields(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
showSettings,
|
||||||
|
onSettingsClick,
|
||||||
|
onFieldsClick,
|
||||||
|
onGroupClick,
|
||||||
|
hidePopup,
|
||||||
|
showAllFields,
|
||||||
|
showGroupFields,
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,28 @@
|
|||||||
|
import { SettingsSvg } from '$app/components/_shared/svg/SettingsSvg';
|
||||||
|
import { useBoardToolbar } from '$app/components/board/BoardToolbar.hooks';
|
||||||
|
import { BoardSettingsPopup } from '$app/components/board/BoardSettingsPopup';
|
||||||
|
import { BoardFieldsPopup } from '$app/components/board/BoardFieldsPopup';
|
||||||
|
import { BoardGroupFieldsPopup } from '$app/components/board/BoardGroupFieldsPopup';
|
||||||
|
|
||||||
|
export const BoardToolbar = ({ title }: { title: string }) => {
|
||||||
|
const { showSettings, showAllFields, showGroupFields, onSettingsClick, onFieldsClick, onGroupClick, hidePopup } =
|
||||||
|
useBoardToolbar();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={'relative flex items-center gap-2'}>
|
||||||
|
<div className={'text-xl font-semibold'}>{title}</div>
|
||||||
|
<button onClick={() => onSettingsClick()} className={'h-5 w-5'}>
|
||||||
|
<SettingsSvg></SettingsSvg>
|
||||||
|
</button>
|
||||||
|
{showSettings && (
|
||||||
|
<BoardSettingsPopup
|
||||||
|
hidePopup={hidePopup}
|
||||||
|
onFieldsClick={onFieldsClick}
|
||||||
|
onGroupClick={onGroupClick}
|
||||||
|
></BoardSettingsPopup>
|
||||||
|
)}
|
||||||
|
{showAllFields && <BoardFieldsPopup hidePopup={hidePopup}></BoardFieldsPopup>}
|
||||||
|
{showGroupFields && <BoardGroupFieldsPopup hidePopup={hidePopup}></BoardGroupFieldsPopup>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -17,12 +17,8 @@ export const BoardUrlCell = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<a
|
<a className={'text-main-accent hover:underline'} href={(data as URLCellDataPB)?.url || ''} target={'_blank'}>
|
||||||
className={'text-main-accent hover:underline'}
|
{(data as URLCellDataPB)?.content || ''}
|
||||||
href={(data as URLCellDataPB | undefined)?.url || ''}
|
|
||||||
target={'_blank'}
|
|
||||||
>
|
|
||||||
{(data as URLCellDataPB | undefined)?.content || ''}
|
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { BlockType } from '@/appflowy_app/interfaces/document';
|
import { BlockType, HeadingBlockData } from '@/appflowy_app/interfaces/document';
|
||||||
import { useAppSelector } from '@/appflowy_app/stores/store';
|
import { useAppSelector } from '@/appflowy_app/stores/store';
|
||||||
import { debounce } from '@/appflowy_app/utils/tool';
|
import { debounce } from '@/appflowy_app/utils/tool';
|
||||||
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
@ -43,9 +43,10 @@ export function useBlockSideTools({ container }: { container: HTMLDivElement })
|
|||||||
el.style.zIndex = '1';
|
el.style.zIndex = '1';
|
||||||
el.style.top = '1px';
|
el.style.top = '1px';
|
||||||
if (node?.type === BlockType.HeadingBlock) {
|
if (node?.type === BlockType.HeadingBlock) {
|
||||||
if (node.data.style?.level === 1) {
|
const nodeData = node.data as HeadingBlockData;
|
||||||
|
if (nodeData.level === 1) {
|
||||||
el.style.top = '8px';
|
el.style.top = '8px';
|
||||||
} else if (node.data.style?.level === 2) {
|
} else if (nodeData.level === 2) {
|
||||||
el.style.top = '6px';
|
el.style.top = '6px';
|
||||||
} else {
|
} else {
|
||||||
el.style.top = '5px';
|
el.style.top = '5px';
|
||||||
@ -80,16 +81,7 @@ function useController() {
|
|||||||
const parentId = node.parent;
|
const parentId = node.parent;
|
||||||
if (!parentId || !controller) return;
|
if (!parentId || !controller) return;
|
||||||
|
|
||||||
controller.transact([
|
//
|
||||||
() => {
|
|
||||||
const newNode = {
|
|
||||||
id: v4(),
|
|
||||||
delta: [],
|
|
||||||
type: BlockType.TextBlock,
|
|
||||||
};
|
|
||||||
controller.insert(newNode, parentId, node.id);
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -3,11 +3,18 @@ import { useDocumentTitle } from './DocumentTitle.hooks';
|
|||||||
import TextBlock from '../TextBlock';
|
import TextBlock from '../TextBlock';
|
||||||
|
|
||||||
export default function DocumentTitle({ id }: { id: string }) {
|
export default function DocumentTitle({ id }: { id: string }) {
|
||||||
const { node, delta } = useDocumentTitle(id);
|
const { node } = useDocumentTitle(id);
|
||||||
if (!node) return null;
|
if (!node) return null;
|
||||||
return (
|
return (
|
||||||
<div data-block-id={node.id} className='doc-title relative pt-[50px] text-4xl font-bold'>
|
<div data-block-id={node.id} className='doc-title relative pt-[50px] text-4xl font-bold'>
|
||||||
<TextBlock placeholder='Untitled' childIds={[]} delta={delta || []} node={node} />
|
<TextBlock placeholder='Untitled' childIds={[]} node={{
|
||||||
|
...node,
|
||||||
|
data: {
|
||||||
|
...node.data,
|
||||||
|
delta: node.data.delta || [],
|
||||||
|
}
|
||||||
|
}} />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ const fontSize: Record<string, string> = {
|
|||||||
export default function HeadingBlock({ node, delta }: { node: Node; delta: TextDelta[] }) {
|
export default function HeadingBlock({ node, delta }: { node: Node; delta: TextDelta[] }) {
|
||||||
return (
|
return (
|
||||||
<div className={`${fontSize[node.data.style?.level]} font-semibold `}>
|
<div className={`${fontSize[node.data.style?.level]} font-semibold `}>
|
||||||
<TextBlock node={node} childIds={[]} delta={delta} />
|
{/*<TextBlock node={node} childIds={[]} delta={delta} />*/}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ export default function ListBlock({ node, delta }: { node: Node; delta: TextDelt
|
|||||||
if (node.data.style?.type === 'column') return <></>;
|
if (node.data.style?.type === 'column') return <></>;
|
||||||
return (
|
return (
|
||||||
<div className='flex-1'>
|
<div className='flex-1'>
|
||||||
<TextBlock delta={delta} node={node} childIds={[]} />
|
{/*<TextBlock delta={delta} node={node} childIds={[]} />*/}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}, [node, delta]);
|
}, [node, delta]);
|
||||||
|
@ -6,7 +6,7 @@ const defaultSize = 60;
|
|||||||
export function useVirtualizedList(count: number) {
|
export function useVirtualizedList(count: number) {
|
||||||
const parentRef = useRef<HTMLDivElement>(null);
|
const parentRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const Virtualize = useVirtualizer({
|
const virtualize = useVirtualizer({
|
||||||
count,
|
count,
|
||||||
getScrollElement: () => parentRef.current,
|
getScrollElement: () => parentRef.current,
|
||||||
estimateSize: () => {
|
estimateSize: () => {
|
||||||
@ -15,7 +15,7 @@ export function useVirtualizedList(count: number) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Virtualize: Virtualize,
|
virtualize,
|
||||||
parentRef,
|
parentRef,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -13,8 +13,8 @@ export default function VirtualizedList({
|
|||||||
node: Node;
|
node: Node;
|
||||||
renderNode: (nodeId: string) => JSX.Element;
|
renderNode: (nodeId: string) => JSX.Element;
|
||||||
}) {
|
}) {
|
||||||
const { Virtualize, parentRef } = useVirtualizedList(childIds.length);
|
const { virtualize, parentRef } = useVirtualizedList(childIds.length);
|
||||||
const virtualItems = Virtualize.getVirtualItems();
|
const virtualItems = virtualize.getVirtualItems();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -25,7 +25,7 @@ export default function VirtualizedList({
|
|||||||
<div
|
<div
|
||||||
className='doc-body max-w-screen w-[900px] min-w-0'
|
className='doc-body max-w-screen w-[900px] min-w-0'
|
||||||
style={{
|
style={{
|
||||||
height: Virtualize.getTotalSize(),
|
height: virtualize.getTotalSize(),
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -42,7 +42,7 @@ export default function VirtualizedList({
|
|||||||
{virtualItems.map((virtualRow) => {
|
{virtualItems.map((virtualRow) => {
|
||||||
const id = childIds[virtualRow.index];
|
const id = childIds[virtualRow.index];
|
||||||
return (
|
return (
|
||||||
<div className='p-[1px]' key={id} data-index={virtualRow.index} ref={Virtualize.measureElement}>
|
<div className='p-[1px]' key={id} data-index={virtualRow.index} ref={virtualize.measureElement}>
|
||||||
{virtualRow.index === 0 ? <DocumentTitle id={node.id} /> : null}
|
{virtualRow.index === 0 ? <DocumentTitle id={node.id} /> : null}
|
||||||
{renderNode(id)}
|
{renderNode(id)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,7 +2,7 @@ import { useAppDispatch, useAppSelector } from '../../stores/store';
|
|||||||
import { errorActions } from '../../stores/reducers/error/slice';
|
import { errorActions } from '../../stores/reducers/error/slice';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export const useError = () => {
|
export const useError = (e: Error) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const error = useAppSelector((state) => state.error);
|
const error = useAppSelector((state) => state.error);
|
||||||
const [errorMessage, setErrorMessage] = useState('');
|
const [errorMessage, setErrorMessage] = useState('');
|
||||||
@ -13,6 +13,12 @@ export const useError = () => {
|
|||||||
setErrorMessage(error.message);
|
setErrorMessage(error.message);
|
||||||
}, [error]);
|
}, [error]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (e) {
|
||||||
|
showError(e.message);
|
||||||
|
}
|
||||||
|
}, [e]);
|
||||||
|
|
||||||
const showError = (msg: string) => {
|
const showError = (msg: string) => {
|
||||||
dispatch(errorActions.showError(msg));
|
dispatch(errorActions.showError(msg));
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { useError } from './Error.hooks';
|
import { useError } from './Error.hooks';
|
||||||
import { ErrorModal } from './ErrorModal';
|
import { ErrorModal } from './ErrorModal';
|
||||||
|
|
||||||
export const ErrorHandlerPage = () => {
|
export const ErrorHandlerPage = ({ error }: { error: Error }) => {
|
||||||
const { hideError, errorMessage, displayError } = useError();
|
const { hideError, errorMessage, displayError } = useError(error);
|
||||||
|
|
||||||
return displayError ? <ErrorModal message={errorMessage} onClose={hideError}></ErrorModal> : <></>;
|
return displayError ? <ErrorModal message={errorMessage} onClose={hideError}></ErrorModal> : <></>;
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,57 @@
|
|||||||
|
import { useDatabase } from '$app/components/_shared/database-hooks/useDatabase';
|
||||||
|
import { GridTableCount } from '../GridTableCount/GridTableCount';
|
||||||
|
import { GridTableHeader } from '../GridTableHeader/GridTableHeader';
|
||||||
|
import { GridAddRow } from '../GridTableRows/GridAddRow';
|
||||||
|
import { GridTableRows } from '../GridTableRows/GridTableRows';
|
||||||
|
import { GridTitle } from '../GridTitle/GridTitle';
|
||||||
|
import { GridToolbar } from '../GridToolbar/GridToolbar';
|
||||||
|
import { EditRow } from '$app/components/_shared/EditRow/EditRow';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { RowInfo } from '$app/stores/effects/database/row/row_cache';
|
||||||
|
import { ViewLayoutPB } from '@/services/backend';
|
||||||
|
|
||||||
|
export const Grid = ({ viewId }: { viewId: string }) => {
|
||||||
|
const { controller, rows, groups } = useDatabase(viewId, ViewLayoutPB.Grid);
|
||||||
|
const [showGridRow, setShowGridRow] = useState(false);
|
||||||
|
const [boardRowInfo, setBoardRowInfo] = useState<RowInfo>();
|
||||||
|
|
||||||
|
const onOpenRow = (rowInfo: RowInfo) => {
|
||||||
|
setBoardRowInfo(rowInfo);
|
||||||
|
setShowGridRow(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{controller && groups && (
|
||||||
|
<>
|
||||||
|
<div className='mx-auto mt-8 flex flex-col gap-8 px-8'>
|
||||||
|
<div className='flex w-full items-center justify-between'>
|
||||||
|
<GridTitle />
|
||||||
|
<GridToolbar />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* table component view with text area for td */}
|
||||||
|
<div className='flex flex-col gap-4'>
|
||||||
|
<table className='w-full table-fixed text-sm'>
|
||||||
|
<GridTableHeader controller={controller} />
|
||||||
|
<GridTableRows onOpenRow={onOpenRow} allRows={rows} viewId={viewId} controller={controller} />
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<GridAddRow controller={controller} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<GridTableCount />
|
||||||
|
</div>
|
||||||
|
{showGridRow && boardRowInfo && (
|
||||||
|
<EditRow
|
||||||
|
onClose={() => setShowGridRow(false)}
|
||||||
|
viewId={viewId}
|
||||||
|
controller={controller}
|
||||||
|
rowInfo={boardRowInfo}
|
||||||
|
></EditRow>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -1,4 +1,3 @@
|
|||||||
import { Link } from 'react-router-dom';
|
|
||||||
import AddSvg from '../../_shared/svg/AddSvg';
|
import AddSvg from '../../_shared/svg/AddSvg';
|
||||||
|
|
||||||
export const GridAddView = () => {
|
export const GridAddView = () => {
|
||||||
|
@ -0,0 +1,44 @@
|
|||||||
|
import { CellIdentifier } from '@/appflowy_app/stores/effects/database/cell/cell_bd_svc';
|
||||||
|
import { CellCache } from '@/appflowy_app/stores/effects/database/cell/cell_cache';
|
||||||
|
import { FieldController } from '@/appflowy_app/stores/effects/database/field/field_controller';
|
||||||
|
import { FieldType } from '@/services/backend';
|
||||||
|
import GridSingleSelectOptions from './GridSingleSelectOptions';
|
||||||
|
import GridTextCell from './GridTextCell';
|
||||||
|
import { GridCheckBox } from './GridCheckBox';
|
||||||
|
import { GridDate } from './GridDate';
|
||||||
|
import { GridUrl } from './GridUrl';
|
||||||
|
import { GridNumberCell } from './GridNumberCell';
|
||||||
|
|
||||||
|
export const GridCell = ({
|
||||||
|
cellIdentifier,
|
||||||
|
cellCache,
|
||||||
|
fieldController,
|
||||||
|
}: {
|
||||||
|
cellIdentifier: CellIdentifier;
|
||||||
|
cellCache: CellCache;
|
||||||
|
fieldController: FieldController;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{cellIdentifier.fieldType === FieldType.MultiSelect ||
|
||||||
|
cellIdentifier.fieldType === FieldType.Checklist ||
|
||||||
|
cellIdentifier.fieldType === FieldType.SingleSelect ? (
|
||||||
|
<GridSingleSelectOptions
|
||||||
|
cellIdentifier={cellIdentifier}
|
||||||
|
cellCache={cellCache}
|
||||||
|
fieldController={fieldController}
|
||||||
|
/>
|
||||||
|
) : cellIdentifier.fieldType === FieldType.Checkbox ? (
|
||||||
|
<GridCheckBox cellIdentifier={cellIdentifier} cellCache={cellCache} fieldController={fieldController} />
|
||||||
|
) : cellIdentifier.fieldType === FieldType.DateTime ? (
|
||||||
|
<GridDate cellIdentifier={cellIdentifier} cellCache={cellCache} fieldController={fieldController}></GridDate>
|
||||||
|
) : cellIdentifier.fieldType === FieldType.URL ? (
|
||||||
|
<GridUrl cellIdentifier={cellIdentifier} cellCache={cellCache} fieldController={fieldController}></GridUrl>
|
||||||
|
) : cellIdentifier.fieldType === FieldType.Number ? (
|
||||||
|
<GridNumberCell cellIdentifier={cellIdentifier} cellCache={cellCache} fieldController={fieldController} />
|
||||||
|
) : (
|
||||||
|
<GridTextCell cellIdentifier={cellIdentifier} cellCache={cellCache} fieldController={fieldController} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,23 @@
|
|||||||
|
import { CellIdentifier } from '@/appflowy_app/stores/effects/database/cell/cell_bd_svc';
|
||||||
|
import { CellCache } from '@/appflowy_app/stores/effects/database/cell/cell_cache';
|
||||||
|
import { FieldController } from '@/appflowy_app/stores/effects/database/field/field_controller';
|
||||||
|
import { EditCheckboxCell } from '../../_shared/EditRow/EditCheckboxCell';
|
||||||
|
import { useCell } from '../../_shared/database-hooks/useCell';
|
||||||
|
|
||||||
|
export const GridCheckBox = ({
|
||||||
|
cellIdentifier,
|
||||||
|
cellCache,
|
||||||
|
fieldController,
|
||||||
|
}: {
|
||||||
|
cellIdentifier: CellIdentifier;
|
||||||
|
cellCache: CellCache;
|
||||||
|
fieldController: FieldController;
|
||||||
|
}) => {
|
||||||
|
const { data, cellController } = useCell(cellIdentifier, cellCache, fieldController);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex w-full justify-start'>
|
||||||
|
{cellController && <EditCheckboxCell cellController={cellController} data={data as 'Yes' | 'No' | undefined} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,47 @@
|
|||||||
|
import { CellIdentifier } from '@/appflowy_app/stores/effects/database/cell/cell_bd_svc';
|
||||||
|
import { CellCache } from '@/appflowy_app/stores/effects/database/cell/cell_cache';
|
||||||
|
import { FieldController } from '@/appflowy_app/stores/effects/database/field/field_controller';
|
||||||
|
import { useCell } from '../../_shared/database-hooks/useCell';
|
||||||
|
import { DateCellDataPB } from '@/services/backend';
|
||||||
|
import { EditCellDate } from '../../_shared/EditRow/EditCellDate';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { DatePickerPopup } from '../../_shared/EditRow/DatePickerPopup';
|
||||||
|
|
||||||
|
export const GridDate = ({
|
||||||
|
cellIdentifier,
|
||||||
|
cellCache,
|
||||||
|
fieldController,
|
||||||
|
}: {
|
||||||
|
cellIdentifier: CellIdentifier;
|
||||||
|
cellCache: CellCache;
|
||||||
|
fieldController: FieldController;
|
||||||
|
}) => {
|
||||||
|
const { data, cellController } = useCell(cellIdentifier, cellCache, fieldController);
|
||||||
|
|
||||||
|
const [showDatePopup, setShowDatePopup] = useState(false);
|
||||||
|
const [datePickerTop, setdatePickerTop] = useState(0);
|
||||||
|
const [datePickerLeft, setdatePickerLeft] = useState(0);
|
||||||
|
|
||||||
|
const onEditDateClick = async (left: number, top: number) => {
|
||||||
|
setdatePickerLeft(left);
|
||||||
|
setdatePickerTop(top);
|
||||||
|
setShowDatePopup(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex w-full cursor-pointer justify-start'>
|
||||||
|
{cellController && <EditCellDate data={data as DateCellDataPB} onEditClick={onEditDateClick}></EditCellDate>}
|
||||||
|
|
||||||
|
{showDatePopup && (
|
||||||
|
<DatePickerPopup
|
||||||
|
top={datePickerTop}
|
||||||
|
left={datePickerLeft}
|
||||||
|
cellIdentifier={cellIdentifier}
|
||||||
|
cellCache={cellCache}
|
||||||
|
fieldController={fieldController}
|
||||||
|
onOutsideClick={() => setShowDatePopup(false)}
|
||||||
|
></DatePickerPopup>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,25 @@
|
|||||||
|
import { CellIdentifier } from '@/appflowy_app/stores/effects/database/cell/cell_bd_svc';
|
||||||
|
import { CellCache } from '@/appflowy_app/stores/effects/database/cell/cell_cache';
|
||||||
|
import { FieldController } from '@/appflowy_app/stores/effects/database/field/field_controller';
|
||||||
|
import { useCell } from '../../_shared/database-hooks/useCell';
|
||||||
|
import { EditCellNumber } from '../../_shared/EditRow/EditCellNumber';
|
||||||
|
|
||||||
|
export const GridNumberCell = ({
|
||||||
|
cellIdentifier,
|
||||||
|
cellCache,
|
||||||
|
fieldController,
|
||||||
|
}: {
|
||||||
|
cellIdentifier: CellIdentifier;
|
||||||
|
cellCache: CellCache;
|
||||||
|
fieldController: FieldController;
|
||||||
|
}) => {
|
||||||
|
const { data, cellController } = useCell(cellIdentifier, cellCache, fieldController);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full'>
|
||||||
|
{cellController && (
|
||||||
|
<EditCellNumber data={data as string | undefined} cellController={cellController}></EditCellNumber>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,75 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { CellOptions } from '$app/components/_shared/EditRow/CellOptions';
|
||||||
|
import { CellIdentifier } from '@/appflowy_app/stores/effects/database/cell/cell_bd_svc';
|
||||||
|
import { CellCache } from '@/appflowy_app/stores/effects/database/cell/cell_cache';
|
||||||
|
import { FieldController } from '@/appflowy_app/stores/effects/database/field/field_controller';
|
||||||
|
import { useCell } from '$app/components/_shared/database-hooks/useCell';
|
||||||
|
import { SelectOptionCellDataPB, SelectOptionPB } from '@/services/backend/models/flowy-database/select_type_option';
|
||||||
|
import { CellOptionsPopup } from '$app/components/_shared/EditRow/CellOptionsPopup';
|
||||||
|
import { EditCellOptionPopup } from '$app/components/_shared/EditRow/EditCellOptionPopup';
|
||||||
|
|
||||||
|
export default function GridSingleSelectOptions({
|
||||||
|
cellIdentifier,
|
||||||
|
cellCache,
|
||||||
|
fieldController,
|
||||||
|
}: {
|
||||||
|
cellIdentifier: CellIdentifier;
|
||||||
|
cellCache: CellCache;
|
||||||
|
fieldController: FieldController;
|
||||||
|
}) {
|
||||||
|
const { data } = useCell(cellIdentifier, cellCache, fieldController);
|
||||||
|
|
||||||
|
const [showOptionsPopup, setShowOptionsPopup] = useState(false);
|
||||||
|
const [changeOptionsTop, setChangeOptionsTop] = useState(0);
|
||||||
|
const [changeOptionsLeft, setChangeOptionsLeft] = useState(0);
|
||||||
|
|
||||||
|
const [showEditCellOption, setShowEditCellOption] = useState(false);
|
||||||
|
const [editCellOptionTop, setEditCellOptionTop] = useState(0);
|
||||||
|
const [editCellOptionLeft, setEditCellOptionLeft] = useState(0);
|
||||||
|
|
||||||
|
const [editingSelectOption, setEditingSelectOption] = useState<SelectOptionPB | undefined>();
|
||||||
|
|
||||||
|
const onEditOptionsClick = async (left: number, top: number) => {
|
||||||
|
setChangeOptionsLeft(left);
|
||||||
|
setChangeOptionsTop(top);
|
||||||
|
setShowOptionsPopup(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onOpenOptionDetailClick = (_left: number, _top: number, _select_option: SelectOptionPB) => {
|
||||||
|
setEditingSelectOption(_select_option);
|
||||||
|
setShowEditCellOption(true);
|
||||||
|
setEditCellOptionLeft(_left);
|
||||||
|
setEditCellOptionTop(_top);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='flex w-full cursor-pointer justify-start'>
|
||||||
|
<CellOptions data={data as SelectOptionCellDataPB} onEditClick={onEditOptionsClick} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showOptionsPopup && (
|
||||||
|
<CellOptionsPopup
|
||||||
|
top={changeOptionsTop}
|
||||||
|
left={changeOptionsLeft}
|
||||||
|
cellIdentifier={cellIdentifier}
|
||||||
|
cellCache={cellCache}
|
||||||
|
fieldController={fieldController}
|
||||||
|
onOutsideClick={() => setShowOptionsPopup(false)}
|
||||||
|
openOptionDetail={onOpenOptionDetailClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showEditCellOption && editingSelectOption && (
|
||||||
|
<EditCellOptionPopup
|
||||||
|
top={editCellOptionTop}
|
||||||
|
left={editCellOptionLeft}
|
||||||
|
cellIdentifier={cellIdentifier}
|
||||||
|
editingSelectOption={editingSelectOption}
|
||||||
|
onOutsideClick={() => {
|
||||||
|
setShowEditCellOption(false);
|
||||||
|
}}
|
||||||
|
></EditCellOptionPopup>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
import { CellIdentifier } from '@/appflowy_app/stores/effects/database/cell/cell_bd_svc';
|
||||||
|
import { CellCache } from '@/appflowy_app/stores/effects/database/cell/cell_cache';
|
||||||
|
import { FieldController } from '@/appflowy_app/stores/effects/database/field/field_controller';
|
||||||
|
import { useCell } from '../../_shared/database-hooks/useCell';
|
||||||
|
import { EditCellText } from '../../_shared/EditRow/EditCellText';
|
||||||
|
|
||||||
|
export default function GridTextCell({
|
||||||
|
cellIdentifier,
|
||||||
|
cellCache,
|
||||||
|
fieldController,
|
||||||
|
}: {
|
||||||
|
cellIdentifier: CellIdentifier;
|
||||||
|
cellCache: CellCache;
|
||||||
|
fieldController: FieldController;
|
||||||
|
}) {
|
||||||
|
const { data, cellController } = useCell(cellIdentifier, cellCache, fieldController);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full'>
|
||||||
|
{cellController && <EditCellText data={data as string | undefined} cellController={cellController}></EditCellText>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
import { CellIdentifier } from '@/appflowy_app/stores/effects/database/cell/cell_bd_svc';
|
||||||
|
import { CellCache } from '@/appflowy_app/stores/effects/database/cell/cell_cache';
|
||||||
|
import { FieldController } from '@/appflowy_app/stores/effects/database/field/field_controller';
|
||||||
|
import { useCell } from '../../_shared/database-hooks/useCell';
|
||||||
|
import { EditCellUrl } from '../../_shared/EditRow/EditCellUrl';
|
||||||
|
import { URLCellDataPB } from '@/services/backend/models/flowy-database/url_type_option_entities';
|
||||||
|
|
||||||
|
export const GridUrl = ({
|
||||||
|
cellIdentifier,
|
||||||
|
cellCache,
|
||||||
|
fieldController,
|
||||||
|
}: {
|
||||||
|
cellIdentifier: CellIdentifier;
|
||||||
|
cellCache: CellCache;
|
||||||
|
fieldController: FieldController;
|
||||||
|
}) => {
|
||||||
|
const { data, cellController } = useCell(cellIdentifier, cellCache, fieldController);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>{cellController && <EditCellUrl data={data as URLCellDataPB} cellController={cellController}></EditCellUrl>}</>
|
||||||
|
);
|
||||||
|
};
|
@ -1,4 +1,4 @@
|
|||||||
import { useAppSelector } from '../../../stores/store';
|
import { useAppSelector } from '$app/stores/store';
|
||||||
|
|
||||||
export const useGridTableCount = () => {
|
export const useGridTableCount = () => {
|
||||||
const { grid } = useAppSelector((state) => state);
|
const { grid } = useAppSelector((state) => state);
|
||||||
|
@ -1,27 +1,25 @@
|
|||||||
import { nanoid } from 'nanoid';
|
import { useAppSelector } from '$app/stores/store';
|
||||||
import { FieldType } from '@/services/backend/models/flowy-database/field_entities';
|
import { DatabaseController } from '@/appflowy_app/stores/effects/database/database_controller';
|
||||||
import { gridActions } from '../../../stores/reducers/grid/slice';
|
import { TypeOptionController } from '@/appflowy_app/stores/effects/database/field/type_option/type_option_controller';
|
||||||
import { useAppDispatch, useAppSelector } from '../../../stores/store';
|
import { None } from 'ts-results';
|
||||||
|
|
||||||
export const useGridTableHeaderHooks = function () {
|
export const useGridTableHeaderHooks = function (controller: DatabaseController) {
|
||||||
const dispatch = useAppDispatch();
|
const database = useAppSelector((state) => state.database);
|
||||||
const grid = useAppSelector((state) => state.grid);
|
|
||||||
|
|
||||||
const onAddField = () => {
|
const onAddField = async () => {
|
||||||
dispatch(
|
// TODO: move this to database controller hook
|
||||||
gridActions.addField({
|
const fieldController = new TypeOptionController(controller.viewId, None);
|
||||||
field: {
|
await fieldController.initialize();
|
||||||
fieldId: nanoid(8),
|
|
||||||
name: 'Name',
|
|
||||||
fieldOptions: {},
|
|
||||||
fieldType: FieldType.RichText,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fields: grid.fields,
|
fields: Object.values(database.fields).map((field) => {
|
||||||
|
return {
|
||||||
|
fieldId: field.fieldId,
|
||||||
|
name: field.title,
|
||||||
|
fieldType: field.fieldType,
|
||||||
|
};
|
||||||
|
}),
|
||||||
onAddField,
|
onAddField,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,49 +1,32 @@
|
|||||||
|
import { DatabaseController } from '@/appflowy_app/stores/effects/database/database_controller';
|
||||||
|
|
||||||
import AddSvg from '../../_shared/svg/AddSvg';
|
import AddSvg from '../../_shared/svg/AddSvg';
|
||||||
import { useGridTableHeaderHooks } from './GridTableHeader.hooks';
|
import { useGridTableHeaderHooks } from './GridTableHeader.hooks';
|
||||||
import { TextTypeSvg } from '../../_shared/svg/TextTypeSvg';
|
|
||||||
import { NumberTypeSvg } from '../../_shared/svg/NumberTypeSvg';
|
|
||||||
import { DateTypeSvg } from '../../_shared/svg/DateTypeSvg';
|
|
||||||
import { SingleSelectTypeSvg } from '../../_shared/svg/SingleSelectTypeSvg';
|
|
||||||
import { MultiSelectTypeSvg } from '../../_shared/svg/MultiSelectTypeSvg';
|
|
||||||
import { ChecklistTypeSvg } from '../../_shared/svg/ChecklistTypeSvg';
|
|
||||||
import { UrlTypeSvg } from '../../_shared/svg/UrlTypeSvg';
|
|
||||||
import { FieldType } from '@/services/backend/models/flowy-database/field_entities';
|
|
||||||
|
|
||||||
export const GridTableHeader = () => {
|
import { GridTableHeaderItem } from './GridTableHeaderItem';
|
||||||
const { fields, onAddField } = useGridTableHeaderHooks();
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
export const GridTableHeader = ({ controller }: { controller: DatabaseController }) => {
|
||||||
|
const { fields, onAddField } = useGridTableHeaderHooks(controller);
|
||||||
|
const { t } = useTranslation('');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
{fields.map((field, i) => {
|
{fields.map((field, i) => {
|
||||||
return (
|
return <GridTableHeaderItem field={field} controller={controller} key={i} />;
|
||||||
<th key={field.fieldId} className='m-0 border border-l-0 border-shade-6 p-0'>
|
|
||||||
<div className={'flex cursor-pointer items-center p-2 hover:bg-main-secondary'}>
|
|
||||||
<i className={'mr-2 h-5 w-5 text-shade-3'}>
|
|
||||||
{field.fieldType === FieldType.RichText && <TextTypeSvg></TextTypeSvg>}
|
|
||||||
{field.fieldType === FieldType.Number && <NumberTypeSvg></NumberTypeSvg>}
|
|
||||||
{field.fieldType === FieldType.DateTime && <DateTypeSvg></DateTypeSvg>}
|
|
||||||
{field.fieldType === FieldType.SingleSelect && <SingleSelectTypeSvg></SingleSelectTypeSvg>}
|
|
||||||
{field.fieldType === FieldType.MultiSelect && <MultiSelectTypeSvg></MultiSelectTypeSvg>}
|
|
||||||
{field.fieldType === FieldType.Checklist && <ChecklistTypeSvg></ChecklistTypeSvg>}
|
|
||||||
{field.fieldType === FieldType.URL && <UrlTypeSvg></UrlTypeSvg>}
|
|
||||||
</i>
|
|
||||||
<span>{field.name}</span>
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<th className='m-0 w-40 border border-r-0 border-shade-6 p-0'>
|
<th className='m-0 w-40 border border-r-0 border-shade-6 p-0'>
|
||||||
<div
|
<div
|
||||||
className='flex cursor-pointer items-center p-2 text-shade-3 hover:bg-main-secondary hover:text-black'
|
className='flex cursor-pointer items-center px-4 py-2 text-shade-3 hover:bg-main-secondary hover:text-black'
|
||||||
onClick={onAddField}
|
onClick={onAddField}
|
||||||
>
|
>
|
||||||
<i className='mr-2 h-5 w-5'>
|
<i className='mr-2 h-5 w-5'>
|
||||||
<AddSvg />
|
<AddSvg />
|
||||||
</i>
|
</i>
|
||||||
<span>New column</span>
|
<span>{t('grid.field.newColumn')}</span>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -0,0 +1,123 @@
|
|||||||
|
import { CellIdentifier } from '@/appflowy_app/stores/effects/database/cell/cell_bd_svc';
|
||||||
|
import { DatabaseController } from '@/appflowy_app/stores/effects/database/database_controller';
|
||||||
|
import { TypeOptionController } from '@/appflowy_app/stores/effects/database/field/type_option/type_option_controller';
|
||||||
|
import { FieldType } from '@/services/backend';
|
||||||
|
import { useState, useRef } from 'react';
|
||||||
|
import { Some } from 'ts-results';
|
||||||
|
import { ChangeFieldTypePopup } from '../../_shared/EditRow/ChangeFieldTypePopup';
|
||||||
|
import { EditFieldPopup } from '../../_shared/EditRow/EditFieldPopup';
|
||||||
|
import { ChecklistTypeSvg } from '../../_shared/svg/ChecklistTypeSvg';
|
||||||
|
import { DateTypeSvg } from '../../_shared/svg/DateTypeSvg';
|
||||||
|
import { MultiSelectTypeSvg } from '../../_shared/svg/MultiSelectTypeSvg';
|
||||||
|
import { NumberTypeSvg } from '../../_shared/svg/NumberTypeSvg';
|
||||||
|
import { SingleSelectTypeSvg } from '../../_shared/svg/SingleSelectTypeSvg';
|
||||||
|
import { TextTypeSvg } from '../../_shared/svg/TextTypeSvg';
|
||||||
|
import { UrlTypeSvg } from '../../_shared/svg/UrlTypeSvg';
|
||||||
|
|
||||||
|
export const GridTableHeaderItem = ({
|
||||||
|
controller,
|
||||||
|
field,
|
||||||
|
}: {
|
||||||
|
controller: DatabaseController;
|
||||||
|
field: {
|
||||||
|
fieldId: string;
|
||||||
|
name: string;
|
||||||
|
fieldType: FieldType;
|
||||||
|
};
|
||||||
|
}) => {
|
||||||
|
const [showFieldEditor, setShowFieldEditor] = useState(false);
|
||||||
|
const [editFieldTop, setEditFieldTop] = useState(0);
|
||||||
|
const [editFieldRight, setEditFieldRight] = useState(0);
|
||||||
|
|
||||||
|
const [showChangeFieldTypePopup, setShowChangeFieldTypePopup] = useState(false);
|
||||||
|
const [changeFieldTypeTop, setChangeFieldTypeTop] = useState(0);
|
||||||
|
const [changeFieldTypeRight, setChangeFieldTypeRight] = useState(0);
|
||||||
|
|
||||||
|
const [editingField, setEditingField] = useState<{
|
||||||
|
fieldId: string;
|
||||||
|
name: string;
|
||||||
|
fieldType: FieldType;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const changeFieldType = async (newType: FieldType) => {
|
||||||
|
if (!editingField) return;
|
||||||
|
|
||||||
|
const currentField = controller.fieldController.getField(editingField.fieldId);
|
||||||
|
if (!currentField) return;
|
||||||
|
|
||||||
|
const typeOptionController = new TypeOptionController(controller.viewId, Some(currentField));
|
||||||
|
await typeOptionController.switchToField(newType);
|
||||||
|
|
||||||
|
setEditingField({
|
||||||
|
...editingField,
|
||||||
|
fieldType: newType,
|
||||||
|
});
|
||||||
|
|
||||||
|
setShowChangeFieldTypePopup(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<th key={field.fieldId} className='m-0 border border-l-0 border-shade-6 p-0'>
|
||||||
|
<div
|
||||||
|
className={'flex w-full cursor-pointer items-center px-4 py-2 hover:bg-main-secondary'}
|
||||||
|
ref={ref}
|
||||||
|
onClick={() => {
|
||||||
|
if (!ref.current) return;
|
||||||
|
const { top, left } = ref.current.getBoundingClientRect();
|
||||||
|
|
||||||
|
setEditFieldRight(left - 10);
|
||||||
|
setEditFieldTop(top + 35);
|
||||||
|
setEditingField(field);
|
||||||
|
setShowFieldEditor(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i className={'mr-2 h-5 w-5 text-shade-3'}>
|
||||||
|
{field.fieldType === FieldType.RichText && <TextTypeSvg></TextTypeSvg>}
|
||||||
|
{field.fieldType === FieldType.Number && <NumberTypeSvg></NumberTypeSvg>}
|
||||||
|
{field.fieldType === FieldType.DateTime && <DateTypeSvg></DateTypeSvg>}
|
||||||
|
{field.fieldType === FieldType.SingleSelect && <SingleSelectTypeSvg></SingleSelectTypeSvg>}
|
||||||
|
{field.fieldType === FieldType.MultiSelect && <MultiSelectTypeSvg></MultiSelectTypeSvg>}
|
||||||
|
{field.fieldType === FieldType.Checklist && <ChecklistTypeSvg></ChecklistTypeSvg>}
|
||||||
|
{field.fieldType === FieldType.Checkbox && <ChecklistTypeSvg></ChecklistTypeSvg>}
|
||||||
|
{field.fieldType === FieldType.URL && <UrlTypeSvg></UrlTypeSvg>}
|
||||||
|
</i>
|
||||||
|
<span>{field.name}</span>
|
||||||
|
|
||||||
|
{showFieldEditor && editingField && (
|
||||||
|
<EditFieldPopup
|
||||||
|
top={editFieldTop}
|
||||||
|
left={editFieldRight}
|
||||||
|
cellIdentifier={
|
||||||
|
{
|
||||||
|
fieldId: editingField.fieldId,
|
||||||
|
fieldType: editingField.fieldType,
|
||||||
|
viewId: controller.viewId,
|
||||||
|
} as CellIdentifier
|
||||||
|
}
|
||||||
|
viewId={controller.viewId}
|
||||||
|
onOutsideClick={() => {
|
||||||
|
setShowFieldEditor(false);
|
||||||
|
}}
|
||||||
|
fieldInfo={controller.fieldController.getField(editingField.fieldId)}
|
||||||
|
changeFieldTypeClick={(buttonTop, buttonRight) => {
|
||||||
|
setChangeFieldTypeTop(buttonTop);
|
||||||
|
setChangeFieldTypeRight(buttonRight);
|
||||||
|
setShowChangeFieldTypePopup(true);
|
||||||
|
}}
|
||||||
|
></EditFieldPopup>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showChangeFieldTypePopup && (
|
||||||
|
<ChangeFieldTypePopup
|
||||||
|
top={changeFieldTypeTop}
|
||||||
|
left={changeFieldTypeRight}
|
||||||
|
onClick={(newType) => changeFieldType(newType)}
|
||||||
|
onOutsideClick={() => setShowChangeFieldTypePopup(false)}
|
||||||
|
></ChangeFieldTypePopup>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
);
|
||||||
|
};
|
@ -1,11 +1,8 @@
|
|||||||
import { gridActions } from '../../../stores/reducers/grid/slice';
|
import { DatabaseController } from '@/appflowy_app/stores/effects/database/database_controller';
|
||||||
import { useAppDispatch } from '../../../stores/store';
|
|
||||||
|
|
||||||
export const useGridAddRow = () => {
|
export const useGridAddRow = (controller: DatabaseController) => {
|
||||||
const dispatch = useAppDispatch();
|
async function addRow() {
|
||||||
|
await controller.createRow();
|
||||||
function addRow() {
|
|
||||||
dispatch(gridActions.addRow());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
|
import { DatabaseController } from '@/appflowy_app/stores/effects/database/database_controller';
|
||||||
import AddSvg from '../../_shared/svg/AddSvg';
|
import AddSvg from '../../_shared/svg/AddSvg';
|
||||||
import { useGridAddRow } from './GridAddRow.hooks';
|
import { useGridAddRow } from './GridAddRow.hooks';
|
||||||
export const GridAddRow = () => {
|
import { useTranslation } from 'react-i18next';
|
||||||
const { addRow } = useGridAddRow();
|
export const GridAddRow = ({ controller }: { controller: DatabaseController }) => {
|
||||||
|
const { addRow } = useGridAddRow(controller);
|
||||||
|
const { t } = useTranslation('');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -9,7 +12,7 @@ export const GridAddRow = () => {
|
|||||||
<i className='mr-2 h-5 w-5'>
|
<i className='mr-2 h-5 w-5'>
|
||||||
<AddSvg />
|
<AddSvg />
|
||||||
</i>
|
</i>
|
||||||
<span>New row</span>
|
<span>{t('grid.row.newRow')}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
import { CellIdentifier } from '@/appflowy_app/stores/effects/database/cell/cell_bd_svc';
|
||||||
|
import { CellCache } from '@/appflowy_app/stores/effects/database/cell/cell_cache';
|
||||||
|
import { FieldController } from '@/appflowy_app/stores/effects/database/field/field_controller';
|
||||||
|
import { GridCell } from '../GridCell/GridCell';
|
||||||
|
|
||||||
|
export const GridTableCell = ({
|
||||||
|
cellIdentifier,
|
||||||
|
cellCache,
|
||||||
|
fieldController,
|
||||||
|
}: {
|
||||||
|
cellIdentifier: CellIdentifier;
|
||||||
|
cellCache: CellCache;
|
||||||
|
fieldController: FieldController;
|
||||||
|
}) => {
|
||||||
|
return <GridCell cellIdentifier={cellIdentifier} cellCache={cellCache} fieldController={fieldController} />;
|
||||||
|
};
|
@ -1,28 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { gridActions } from '../../../stores/reducers/grid/slice';
|
|
||||||
import { useAppDispatch, useAppSelector } from '../../../stores/store';
|
|
||||||
|
|
||||||
export const useGridTableItemHooks = (
|
|
||||||
rowItem: { value: string | number; fieldId: string; cellId: string },
|
|
||||||
rowId: string
|
|
||||||
) => {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const [value, setValue] = useState(rowItem.value);
|
|
||||||
|
|
||||||
function onValueChange(event: React.ChangeEvent<HTMLInputElement>) {
|
|
||||||
setValue(event.target.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onValueBlur() {
|
|
||||||
dispatch(gridActions.updateRowValue({ rowId: rowId, cellId: rowItem.cellId, value }));
|
|
||||||
}
|
|
||||||
|
|
||||||
const grid = useAppSelector((state) => state.grid);
|
|
||||||
|
|
||||||
return {
|
|
||||||
rows: grid.rows,
|
|
||||||
onValueChange,
|
|
||||||
value,
|
|
||||||
onValueBlur,
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,26 +0,0 @@
|
|||||||
import { useGridTableItemHooks } from './GridTableItem.hooks';
|
|
||||||
|
|
||||||
export const GridTableItem = ({
|
|
||||||
rowItem,
|
|
||||||
rowId,
|
|
||||||
}: {
|
|
||||||
rowItem: {
|
|
||||||
fieldId: string;
|
|
||||||
value: string | number;
|
|
||||||
cellId: string;
|
|
||||||
};
|
|
||||||
rowId: string;
|
|
||||||
}) => {
|
|
||||||
const { value, onValueChange, onValueBlur } = useGridTableItemHooks(rowItem, rowId);
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
className='h-full w-full rounded-lg border border-transparent p-2 hover:border-main-accent'
|
|
||||||
type='text'
|
|
||||||
value={value}
|
|
||||||
onChange={onValueChange}
|
|
||||||
onBlur={onValueBlur}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -0,0 +1,46 @@
|
|||||||
|
import { DatabaseController } from '@/appflowy_app/stores/effects/database/database_controller';
|
||||||
|
import { RowInfo } from '@/appflowy_app/stores/effects/database/row/row_cache';
|
||||||
|
import { useRow } from '../../_shared/database-hooks/useRow';
|
||||||
|
import { FullView } from '../../_shared/svg/FullView';
|
||||||
|
import { GridCell } from '../GridCell/GridCell';
|
||||||
|
|
||||||
|
export const GridTableRow = ({
|
||||||
|
viewId,
|
||||||
|
controller,
|
||||||
|
row,
|
||||||
|
onOpenRow,
|
||||||
|
}: {
|
||||||
|
viewId: string;
|
||||||
|
controller: DatabaseController;
|
||||||
|
row: RowInfo;
|
||||||
|
onOpenRow: (rowId: RowInfo) => void;
|
||||||
|
}) => {
|
||||||
|
const { cells } = useRow(viewId, controller, row);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr className='group'>
|
||||||
|
{cells.map((cell, cellIndex) => {
|
||||||
|
return (
|
||||||
|
<td className='m-0 border border-l-0 border-shade-6 p-0 ' key={cellIndex}>
|
||||||
|
<div className='flex w-full items-center justify-end'>
|
||||||
|
<GridCell
|
||||||
|
cellIdentifier={cell.cellIdentifier}
|
||||||
|
cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
|
||||||
|
fieldController={controller.fieldController}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{cellIndex === 0 && (
|
||||||
|
<div
|
||||||
|
onClick={() => onOpenRow(row)}
|
||||||
|
className='mr-1 hidden h-9 w-9 cursor-pointer rounded p-2 hover:bg-slate-200 group-hover:block '
|
||||||
|
>
|
||||||
|
<FullView />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
};
|
@ -1,9 +0,0 @@
|
|||||||
import { useAppSelector } from '../../../stores/store';
|
|
||||||
|
|
||||||
export const useGridTableRowsHooks = () => {
|
|
||||||
const grid = useAppSelector((state) => state.grid);
|
|
||||||
|
|
||||||
return {
|
|
||||||
rows: grid.rows,
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,24 +1,21 @@
|
|||||||
import { GridTableItem } from './GridTableItem';
|
import { DatabaseController } from '@/appflowy_app/stores/effects/database/database_controller';
|
||||||
import { useGridTableRowsHooks } from './GridTableRows.hooks';
|
import { RowInfo } from '@/appflowy_app/stores/effects/database/row/row_cache';
|
||||||
|
import { GridTableRow } from './GridTableRow';
|
||||||
export const GridTableRows = () => {
|
export const GridTableRows = ({
|
||||||
const { rows } = useGridTableRowsHooks();
|
viewId,
|
||||||
|
controller,
|
||||||
|
allRows,
|
||||||
|
onOpenRow,
|
||||||
|
}: {
|
||||||
|
viewId: string;
|
||||||
|
controller: DatabaseController;
|
||||||
|
allRows: readonly RowInfo[];
|
||||||
|
onOpenRow: (rowId: RowInfo) => void;
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<tbody>
|
<tbody>
|
||||||
{rows.map((row, i) => {
|
{allRows.map((row, i) => {
|
||||||
return (
|
return <GridTableRow onOpenRow={onOpenRow} row={row} key={i} viewId={viewId} controller={controller} />;
|
||||||
<tr key={row.rowId}>
|
|
||||||
{row.values.map((value) => {
|
|
||||||
return (
|
|
||||||
<td key={value.fieldId} className='m-0 border border-l-0 border-shade-6 p-0'>
|
|
||||||
<GridTableItem rowItem={value} rowId={row.rowId} />
|
|
||||||
</td>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
<td className='m-0 border border-r-0 border-shade-6 p-0'></td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
);
|
);
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
|
import { useAppDispatch, useAppSelector } from '@/appflowy_app/stores/store';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { gridActions } from '../../../stores/reducers/grid/slice';
|
|
||||||
|
|
||||||
import { useAppDispatch, useAppSelector } from '../../../stores/store';
|
|
||||||
|
|
||||||
export const useGridTitleHooks = function () {
|
export const useGridTitleHooks = function () {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@ -9,16 +7,12 @@ export const useGridTitleHooks = function () {
|
|||||||
|
|
||||||
const [title, setTitle] = useState(grid.title);
|
const [title, setTitle] = useState(grid.title);
|
||||||
const [changingTitle, setChangingTitle] = useState(false);
|
const [changingTitle, setChangingTitle] = useState(false);
|
||||||
|
const [showOptions, setShowOptions] = useState(false);
|
||||||
|
|
||||||
const onTitleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
const onTitleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
setTitle(event.target.value);
|
setTitle(event.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onTitleBlur = () => {
|
|
||||||
dispatch(gridActions.updateGridTitle({ title }));
|
|
||||||
setChangingTitle(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onTitleClick = () => {
|
const onTitleClick = () => {
|
||||||
setChangingTitle(true);
|
setChangingTitle(true);
|
||||||
};
|
};
|
||||||
@ -26,8 +20,9 @@ export const useGridTitleHooks = function () {
|
|||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
onTitleChange,
|
onTitleChange,
|
||||||
onTitleBlur,
|
|
||||||
onTitleClick,
|
onTitleClick,
|
||||||
changingTitle,
|
changingTitle,
|
||||||
|
showOptions,
|
||||||
|
setShowOptions,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,15 +1,21 @@
|
|||||||
import { useGridTitleHooks } from './GridTitle.hooks';
|
import { useGridTitleHooks } from './GridTitle.hooks';
|
||||||
import { SettingsSvg } from '../../_shared/svg/SettingsSvg';
|
import { SettingsSvg } from '../../_shared/svg/SettingsSvg';
|
||||||
|
import { GridTitleOptionsPopup } from './GridTitleOptionsPopup';
|
||||||
|
|
||||||
export const GridTitle = () => {
|
export const GridTitle = () => {
|
||||||
const { title } = useGridTitleHooks();
|
const { title, showOptions, setShowOptions } = useGridTitleHooks();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex items-center text-xl font-semibold'}>
|
<div className={'relative flex items-center '}>
|
||||||
<div>{title}</div>
|
<div>{title}</div>
|
||||||
<button className={'ml-2 h-5 w-5'}>
|
|
||||||
<SettingsSvg></SettingsSvg>
|
<div className='flex '>
|
||||||
</button>
|
<button className={'ml-2 h-5 w-5 '} onClick={() => setShowOptions(!showOptions)}>
|
||||||
|
<SettingsSvg></SettingsSvg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{showOptions && <GridTitleOptionsPopup onClose={() => setShowOptions(!showOptions)} />}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,55 @@
|
|||||||
|
import { IPopupItem, PopupSelect } from '../../_shared/PopupSelect';
|
||||||
|
import { FilterSvg } from '../../_shared/svg/FilterSvg';
|
||||||
|
import { GroupBySvg } from '../../_shared/svg/GroupBySvg';
|
||||||
|
import { PropertiesSvg } from '../../_shared/svg/PropertiesSvg';
|
||||||
|
import { SortSvg } from '../../_shared/svg/SortSvg';
|
||||||
|
|
||||||
|
export const GridTitleOptionsPopup = ({ onClose }: { onClose?: () => void }) => {
|
||||||
|
const items: IPopupItem[] = [
|
||||||
|
{
|
||||||
|
icon: (
|
||||||
|
<i className={'h-[16px] w-[16px] text-black'}>
|
||||||
|
<FilterSvg />
|
||||||
|
</i>
|
||||||
|
),
|
||||||
|
onClick: () => {
|
||||||
|
console.log('filter');
|
||||||
|
},
|
||||||
|
title: 'Filter',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: (
|
||||||
|
<i className={'h-[16px] w-[16px] text-black'}>
|
||||||
|
<SortSvg />
|
||||||
|
</i>
|
||||||
|
),
|
||||||
|
onClick: () => {
|
||||||
|
console.log('sort');
|
||||||
|
},
|
||||||
|
title: 'Sort',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: (
|
||||||
|
<i className={'h-[16px] w-[16px] text-black'}>
|
||||||
|
<PropertiesSvg />
|
||||||
|
</i>
|
||||||
|
),
|
||||||
|
onClick: () => {
|
||||||
|
console.log('fields');
|
||||||
|
},
|
||||||
|
title: 'Fields',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: (
|
||||||
|
<i className={'h-[16px] w-[16px] text-black'}>
|
||||||
|
<GroupBySvg />
|
||||||
|
</i>
|
||||||
|
),
|
||||||
|
onClick: () => {
|
||||||
|
console.log('group by');
|
||||||
|
},
|
||||||
|
title: 'Group by',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return <PopupSelect items={items} className={'absolute top-full z-10 w-fit'} onOutsideClick={onClose} />;
|
||||||
|
};
|
@ -1,17 +1,11 @@
|
|||||||
import { GridAddView } from '../GridAddView/GridAddView';
|
import { GridAddView } from '../GridAddView/GridAddView';
|
||||||
import { SearchInput } from '../../_shared/SearchInput';
|
import { SearchInput } from '../../_shared/SearchInput';
|
||||||
import { GridSortButton } from './GridSortButton';
|
|
||||||
import { GridFieldsButton } from './GridFieldsButton';
|
|
||||||
import { GridFilterButton } from './GridFilterButton';
|
|
||||||
|
|
||||||
export const GridToolbar = () => {
|
export const GridToolbar = () => {
|
||||||
return (
|
return (
|
||||||
<div className='flex shrink-0 items-center gap-4'>
|
<div className='flex shrink-0 items-center gap-4'>
|
||||||
<SearchInput />
|
<SearchInput />
|
||||||
<GridAddView />
|
<GridAddView />
|
||||||
<GridFilterButton></GridFilterButton>
|
|
||||||
<GridSortButton></GridSortButton>
|
|
||||||
<GridFieldsButton></GridFieldsButton>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { IPopupItem, Popup } from '../../_shared/Popup';
|
import { IPopupItem, PopupSelect } from '../../_shared/PopupSelect';
|
||||||
import { LogoutSvg } from '../../_shared/svg/LogoutSvg';
|
import { LogoutSvg } from '../../_shared/svg/LogoutSvg';
|
||||||
|
|
||||||
export const OptionsPopup = ({ onSignOutClick, onClose }: { onSignOutClick: () => void; onClose: () => void }) => {
|
export const OptionsPopup = ({ onSignOutClick, onClose }: { onSignOutClick: () => void; onClose: () => void }) => {
|
||||||
@ -14,10 +14,10 @@ export const OptionsPopup = ({ onSignOutClick, onClose }: { onSignOutClick: () =
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<Popup
|
<PopupSelect
|
||||||
className={'absolute top-[50px] right-[30px] z-10 whitespace-nowrap'}
|
className={'absolute top-[50px] right-[30px] z-10 whitespace-nowrap'}
|
||||||
items={items}
|
items={items}
|
||||||
onOutsideClick={onClose}
|
onOutsideClick={onClose}
|
||||||
></Popup>
|
></PopupSelect>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -5,7 +5,7 @@ import { IPage, pagesActions } from '../../../stores/reducers/pages/slice';
|
|||||||
import { ViewLayoutPB } from '@/services/backend';
|
import { ViewLayoutPB } from '@/services/backend';
|
||||||
import { AppBackendService } from '../../../stores/effects/folder/app/app_bd_svc';
|
import { AppBackendService } from '../../../stores/effects/folder/app/app_bd_svc';
|
||||||
import { WorkspaceBackendService } from '../../../stores/effects/folder/workspace/workspace_bd_svc';
|
import { WorkspaceBackendService } from '../../../stores/effects/folder/workspace/workspace_bd_svc';
|
||||||
import { useError } from '../../error/Error.hooks';
|
|
||||||
import { AppObserver } from '../../../stores/effects/folder/app/app_observer';
|
import { AppObserver } from '../../../stores/effects/folder/app/app_observer';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { INITIAL_FOLDER_HEIGHT, PAGE_ITEM_HEIGHT } from '../../_shared/constants';
|
import { INITIAL_FOLDER_HEIGHT, PAGE_ITEM_HEIGHT } from '../../_shared/constants';
|
||||||
@ -32,9 +32,6 @@ export const useFolderEvents = (folder: IFolder, pages: IPage[]) => {
|
|||||||
const appBackendService = new AppBackendService(folder.id);
|
const appBackendService = new AppBackendService(folder.id);
|
||||||
const workspaceBackendService = new WorkspaceBackendService(workspace.id || '');
|
const workspaceBackendService = new WorkspaceBackendService(workspace.id || '');
|
||||||
|
|
||||||
// Error
|
|
||||||
const error = useError();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void appObserver.subscribe({
|
void appObserver.subscribe({
|
||||||
onAppChanged: (change) => {
|
onAppChanged: (change) => {
|
||||||
@ -85,12 +82,8 @@ export const useFolderEvents = (folder: IFolder, pages: IPage[]) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const changeFolderTitle = async (newTitle: string) => {
|
const changeFolderTitle = async (newTitle: string) => {
|
||||||
try {
|
await appBackendService.update({ name: newTitle });
|
||||||
await appBackendService.update({ name: newTitle });
|
appDispatch(foldersActions.renameFolder({ id: folder.id, newTitle }));
|
||||||
appDispatch(foldersActions.renameFolder({ id: folder.id, newTitle }));
|
|
||||||
} catch (e: any) {
|
|
||||||
error.showError(e?.message);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeRenamePopup = () => {
|
const closeRenamePopup = () => {
|
||||||
@ -99,24 +92,16 @@ export const useFolderEvents = (folder: IFolder, pages: IPage[]) => {
|
|||||||
|
|
||||||
const deleteFolder = async () => {
|
const deleteFolder = async () => {
|
||||||
closePopup();
|
closePopup();
|
||||||
try {
|
await appBackendService.delete();
|
||||||
await appBackendService.delete();
|
appDispatch(foldersActions.deleteFolder({ id: folder.id }));
|
||||||
appDispatch(foldersActions.deleteFolder({ id: folder.id }));
|
|
||||||
} catch (e: any) {
|
|
||||||
error.showError(e?.message);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const duplicateFolder = async () => {
|
const duplicateFolder = async () => {
|
||||||
closePopup();
|
closePopup();
|
||||||
try {
|
const newApp = await workspaceBackendService.createApp({
|
||||||
const newApp = await workspaceBackendService.createApp({
|
name: folder.title,
|
||||||
name: folder.title,
|
});
|
||||||
});
|
appDispatch(foldersActions.addFolder({ id: newApp.id, title: folder.title }));
|
||||||
appDispatch(foldersActions.addFolder({ id: newApp.id, title: folder.title }));
|
|
||||||
} catch (e: any) {
|
|
||||||
error.showError(e?.message);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const closePopup = () => {
|
const closePopup = () => {
|
||||||
@ -126,77 +111,65 @@ export const useFolderEvents = (folder: IFolder, pages: IPage[]) => {
|
|||||||
|
|
||||||
const onAddNewDocumentPage = async () => {
|
const onAddNewDocumentPage = async () => {
|
||||||
closePopup();
|
closePopup();
|
||||||
try {
|
const newView = await appBackendService.createView({
|
||||||
const newView = await appBackendService.createView({
|
name: 'New Document 1',
|
||||||
name: 'New Document 1',
|
layoutType: ViewLayoutPB.Document,
|
||||||
layoutType: ViewLayoutPB.Document,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
appDispatch(
|
appDispatch(
|
||||||
pagesActions.addPage({
|
pagesActions.addPage({
|
||||||
folderId: folder.id,
|
folderId: folder.id,
|
||||||
pageType: ViewLayoutPB.Document,
|
pageType: ViewLayoutPB.Document,
|
||||||
title: newView.name,
|
title: newView.name,
|
||||||
id: newView.id,
|
id: newView.id,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
setShowPages(true);
|
setShowPages(true);
|
||||||
|
|
||||||
navigate(`/page/document/${newView.id}`);
|
navigate(`/page/document/${newView.id}`);
|
||||||
} catch (e: any) {
|
|
||||||
error.showError(e?.message);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onAddNewBoardPage = async () => {
|
const onAddNewBoardPage = async () => {
|
||||||
closePopup();
|
closePopup();
|
||||||
try {
|
const newView = await appBackendService.createView({
|
||||||
const newView = await appBackendService.createView({
|
name: 'New Board 1',
|
||||||
name: 'New Board 1',
|
layoutType: ViewLayoutPB.Board,
|
||||||
layoutType: ViewLayoutPB.Board,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
setShowPages(true);
|
setShowPages(true);
|
||||||
|
|
||||||
appDispatch(
|
appDispatch(
|
||||||
pagesActions.addPage({
|
pagesActions.addPage({
|
||||||
folderId: folder.id,
|
folderId: folder.id,
|
||||||
pageType: ViewLayoutPB.Board,
|
pageType: ViewLayoutPB.Board,
|
||||||
title: newView.name,
|
title: newView.name,
|
||||||
id: newView.id,
|
id: newView.id,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
navigate(`/page/board/${newView.id}`);
|
navigate(`/page/board/${newView.id}`);
|
||||||
} catch (e: any) {
|
|
||||||
error.showError(e?.message);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onAddNewGridPage = async () => {
|
const onAddNewGridPage = async () => {
|
||||||
closePopup();
|
closePopup();
|
||||||
try {
|
const newView = await appBackendService.createView({
|
||||||
const newView = await appBackendService.createView({
|
name: 'New Grid 1',
|
||||||
name: 'New Grid 1',
|
layoutType: ViewLayoutPB.Grid,
|
||||||
layoutType: ViewLayoutPB.Grid,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
setShowPages(true);
|
setShowPages(true);
|
||||||
|
|
||||||
appDispatch(
|
appDispatch(
|
||||||
pagesActions.addPage({
|
pagesActions.addPage({
|
||||||
folderId: folder.id,
|
folderId: folder.id,
|
||||||
pageType: ViewLayoutPB.Grid,
|
pageType: ViewLayoutPB.Grid,
|
||||||
title: newView.name,
|
title: newView.name,
|
||||||
id: newView.id,
|
id: newView.id,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
navigate(`/page/grid/${newView.id}`);
|
navigate(`/page/grid/${newView.id}`);
|
||||||
} catch (e: any) {
|
|
||||||
error.showError(e?.message);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { IPopupItem, Popup } from '../../_shared/Popup';
|
import { IPopupItem, PopupSelect } from '../../_shared/PopupSelect';
|
||||||
import { EditSvg } from '../../_shared/svg/EditSvg';
|
import { EditSvg } from '../../_shared/svg/EditSvg';
|
||||||
import { TrashSvg } from '../../_shared/svg/TrashSvg';
|
import { TrashSvg } from '../../_shared/svg/TrashSvg';
|
||||||
import { CopySvg } from '../../_shared/svg/CopySvg';
|
import { CopySvg } from '../../_shared/svg/CopySvg';
|
||||||
@ -47,11 +47,11 @@ export const NavItemOptionsPopup = ({
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popup
|
<PopupSelect
|
||||||
onOutsideClick={() => onClose && onClose()}
|
onOutsideClick={() => onClose && onClose()}
|
||||||
items={items}
|
items={items}
|
||||||
className={`absolute right-0`}
|
className={`absolute right-0`}
|
||||||
style={{ top: `${top}px` }}
|
style={{ top: `${top}px` }}
|
||||||
></Popup>
|
></PopupSelect>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -117,6 +117,7 @@ export const NavigationPanel = ({
|
|||||||
{/*<PluginsButton></PluginsButton>*/}
|
{/*<PluginsButton></PluginsButton>*/}
|
||||||
|
|
||||||
<DesignSpec></DesignSpec>
|
<DesignSpec></DesignSpec>
|
||||||
|
<AllIcons></AllIcons>
|
||||||
<TestBackendButton></TestBackendButton>
|
<TestBackendButton></TestBackendButton>
|
||||||
|
|
||||||
{/*Trash Button*/}
|
{/*Trash Button*/}
|
||||||
@ -158,7 +159,7 @@ export const TestBackendButton = () => {
|
|||||||
onClick={() => navigate('/page/api-test')}
|
onClick={() => navigate('/page/api-test')}
|
||||||
className={'flex w-full items-center rounded-lg px-4 py-2 hover:bg-surface-2'}
|
className={'flex w-full items-center rounded-lg px-4 py-2 hover:bg-surface-2'}
|
||||||
>
|
>
|
||||||
APITest
|
API Test
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -171,7 +172,19 @@ export const DesignSpec = () => {
|
|||||||
onClick={() => navigate('page/colors')}
|
onClick={() => navigate('page/colors')}
|
||||||
className={'flex w-full items-center rounded-lg px-4 py-2 hover:bg-surface-2'}
|
className={'flex w-full items-center rounded-lg px-4 py-2 hover:bg-surface-2'}
|
||||||
>
|
>
|
||||||
Design Specs
|
Color Palette
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AllIcons = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('page/all-icons')}
|
||||||
|
className={'flex w-full items-center rounded-lg px-4 py-2 hover:bg-surface-2'}
|
||||||
|
>
|
||||||
|
All Icons
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,23 +1,17 @@
|
|||||||
import { useAppDispatch, useAppSelector } from '../../../stores/store';
|
import { useAppDispatch, useAppSelector } from '../../../stores/store';
|
||||||
import { foldersActions } from '../../../stores/reducers/folders/slice';
|
import { foldersActions } from '../../../stores/reducers/folders/slice';
|
||||||
import { WorkspaceBackendService } from '../../../stores/effects/folder/workspace/workspace_bd_svc';
|
import { WorkspaceBackendService } from '../../../stores/effects/folder/workspace/workspace_bd_svc';
|
||||||
import { useError } from '../../error/Error.hooks';
|
|
||||||
|
|
||||||
export const useNewFolder = () => {
|
export const useNewFolder = () => {
|
||||||
const appDispatch = useAppDispatch();
|
const appDispatch = useAppDispatch();
|
||||||
const workspace = useAppSelector((state) => state.workspace);
|
const workspace = useAppSelector((state) => state.workspace);
|
||||||
const workspaceBackendService = new WorkspaceBackendService(workspace.id || '');
|
const workspaceBackendService = new WorkspaceBackendService(workspace.id || '');
|
||||||
const error = useError();
|
|
||||||
|
|
||||||
const onNewFolder = async () => {
|
const onNewFolder = async () => {
|
||||||
try {
|
const newApp = await workspaceBackendService.createApp({
|
||||||
const newApp = await workspaceBackendService.createApp({
|
name: 'New Folder 1',
|
||||||
name: 'New Folder 1',
|
});
|
||||||
});
|
appDispatch(foldersActions.addFolder({ id: newApp.id, title: newApp.name }));
|
||||||
appDispatch(foldersActions.addFolder({ id: newApp.id, title: newApp.name }));
|
|
||||||
} catch (e: any) {
|
|
||||||
error.showError(e?.message);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { IPopupItem, Popup } from '../../_shared/Popup';
|
import { IPopupItem, PopupSelect } from '../../_shared/PopupSelect';
|
||||||
import { DocumentSvg } from '../../_shared/svg/DocumentSvg';
|
import { DocumentSvg } from '../../_shared/svg/DocumentSvg';
|
||||||
import { BoardSvg } from '../../_shared/svg/BoardSvg';
|
import { BoardSvg } from '../../_shared/svg/BoardSvg';
|
||||||
import { GridSvg } from '../../_shared/svg/GridSvg';
|
import { GridSvg } from '../../_shared/svg/GridSvg';
|
||||||
@ -47,11 +47,11 @@ export const NewPagePopup = ({
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popup
|
<PopupSelect
|
||||||
onOutsideClick={() => onClose && onClose()}
|
onOutsideClick={() => onClose && onClose()}
|
||||||
items={items}
|
items={items}
|
||||||
className={'absolute right-0'}
|
className={'absolute right-0'}
|
||||||
style={{ top: `${top}px` }}
|
style={{ top: `${top}px` }}
|
||||||
></Popup>
|
></PopupSelect>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -13,7 +13,6 @@ export const usePageEvents = (page: IPage) => {
|
|||||||
const [activePageId, setActivePageId] = useState<string>('');
|
const [activePageId, setActivePageId] = useState<string>('');
|
||||||
const currentLocation = useLocation();
|
const currentLocation = useLocation();
|
||||||
const viewBackendService: ViewBackendService = new ViewBackendService(page.id);
|
const viewBackendService: ViewBackendService = new ViewBackendService(page.id);
|
||||||
const error = useError();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { pathname } = currentLocation;
|
const { pathname } = currentLocation;
|
||||||
@ -32,33 +31,21 @@ export const usePageEvents = (page: IPage) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const changePageTitle = async (newTitle: string) => {
|
const changePageTitle = async (newTitle: string) => {
|
||||||
try {
|
await viewBackendService.update({ name: newTitle });
|
||||||
await viewBackendService.update({ name: newTitle });
|
appDispatch(pagesActions.renamePage({ id: page.id, newTitle }));
|
||||||
appDispatch(pagesActions.renamePage({ id: page.id, newTitle }));
|
|
||||||
} catch (e: any) {
|
|
||||||
error.showError(e?.message);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const deletePage = async () => {
|
const deletePage = async () => {
|
||||||
closePopup();
|
closePopup();
|
||||||
try {
|
await viewBackendService.delete();
|
||||||
await viewBackendService.delete();
|
appDispatch(pagesActions.deletePage({ id: page.id }));
|
||||||
appDispatch(pagesActions.deletePage({ id: page.id }));
|
|
||||||
} catch (e: any) {
|
|
||||||
error.showError(e?.message);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const duplicatePage = () => {
|
const duplicatePage = () => {
|
||||||
closePopup();
|
closePopup();
|
||||||
try {
|
appDispatch(
|
||||||
appDispatch(
|
pagesActions.addPage({ id: nanoid(8), pageType: page.pageType, title: page.title, folderId: page.folderId })
|
||||||
pagesActions.addPage({ id: nanoid(8), pageType: page.pageType, title: page.title, folderId: page.folderId })
|
);
|
||||||
);
|
|
||||||
} catch (e: any) {
|
|
||||||
error.showError(e?.message);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const closePopup = () => {
|
const closePopup = () => {
|
||||||
|
@ -3,13 +3,12 @@ import { useAppDispatch, useAppSelector } from '../../stores/store';
|
|||||||
import { pagesActions } from '../../stores/reducers/pages/slice';
|
import { pagesActions } from '../../stores/reducers/pages/slice';
|
||||||
import { workspaceActions } from '../../stores/reducers/workspace/slice';
|
import { workspaceActions } from '../../stores/reducers/workspace/slice';
|
||||||
import { UserBackendService } from '../../stores/effects/user/user_bd_svc';
|
import { UserBackendService } from '../../stores/effects/user/user_bd_svc';
|
||||||
import { useError } from '../error/Error.hooks';
|
|
||||||
|
|
||||||
export const useWorkspace = () => {
|
export const useWorkspace = () => {
|
||||||
const currentUser = useAppSelector((state) => state.currentUser);
|
const currentUser = useAppSelector((state) => state.currentUser);
|
||||||
|
|
||||||
const appDispatch = useAppDispatch();
|
const appDispatch = useAppDispatch();
|
||||||
const error = useError();
|
|
||||||
const userBackendService: UserBackendService = new UserBackendService(currentUser.id || 0);
|
const userBackendService: UserBackendService = new UserBackendService(currentUser.id || 0);
|
||||||
|
|
||||||
const loadWorkspaceItems = async () => {
|
const loadWorkspaceItems = async () => {
|
||||||
@ -31,15 +30,11 @@ export const useWorkspace = () => {
|
|||||||
}
|
}
|
||||||
} catch (e1) {
|
} catch (e1) {
|
||||||
// create workspace for first start
|
// create workspace for first start
|
||||||
try {
|
const workspace = await userBackendService.createWorkspace({ name: 'New Workspace', desc: '' });
|
||||||
const workspace = await userBackendService.createWorkspace({ name: 'New Workspace', desc: '' });
|
appDispatch(workspaceActions.updateWorkspace({ id: workspace.id, name: workspace.name }));
|
||||||
appDispatch(workspaceActions.updateWorkspace({ id: workspace.id, name: workspace.name }));
|
|
||||||
|
|
||||||
appDispatch(foldersActions.clearFolders());
|
appDispatch(foldersActions.clearFolders());
|
||||||
appDispatch(pagesActions.clearPages());
|
appDispatch(pagesActions.clearPages());
|
||||||
} catch (e2: any) {
|
|
||||||
error.showError(e2?.message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -0,0 +1,168 @@
|
|||||||
|
import AddSvg from '$app/components/_shared/svg/AddSvg';
|
||||||
|
import { ArrowLeftSvg } from '$app/components/_shared/svg/ArrowLeftSvg';
|
||||||
|
import { ArrowRightSvg } from '$app/components/_shared/svg/ArrowRightSvg';
|
||||||
|
import { BoardSvg } from '$app/components/_shared/svg/BoardSvg';
|
||||||
|
import { CheckboxSvg } from '$app/components/_shared/svg/CheckboxSvg';
|
||||||
|
import { ChecklistTypeSvg } from '$app/components/_shared/svg/ChecklistTypeSvg';
|
||||||
|
import { CheckmarkSvg } from '$app/components/_shared/svg/CheckmarkSvg';
|
||||||
|
import { ClockSvg } from '$app/components/_shared/svg/ClockSvg';
|
||||||
|
import { CloseSvg } from '$app/components/_shared/svg/CloseSvg';
|
||||||
|
import { CopySvg } from '$app/components/_shared/svg/CopySvg';
|
||||||
|
import { DateTypeSvg } from '$app/components/_shared/svg/DateTypeSvg';
|
||||||
|
import { Details2Svg } from '$app/components/_shared/svg/Details2Svg';
|
||||||
|
import { DocumentSvg } from '$app/components/_shared/svg/DocumentSvg';
|
||||||
|
import { DropDownShowSvg } from '$app/components/_shared/svg/DropDownShowSvg';
|
||||||
|
import { EarthSvg } from '$app/components/_shared/svg/EarthSvg';
|
||||||
|
import { EditorCheckSvg } from '$app/components/_shared/svg/EditorCheckSvg';
|
||||||
|
import { EditorUncheckSvg } from '$app/components/_shared/svg/EditorUncheckSvg';
|
||||||
|
import { EditSvg } from '$app/components/_shared/svg/EditSvg';
|
||||||
|
import { EyeClosedSvg } from '$app/components/_shared/svg/EyeClosedSvg';
|
||||||
|
import { EyeOpenSvg } from '$app/components/_shared/svg/EyeOpenSvg';
|
||||||
|
import { FilterSvg } from '$app/components/_shared/svg/FilterSvg';
|
||||||
|
import { GridSvg } from '$app/components/_shared/svg/GridSvg';
|
||||||
|
import { GroupByFieldSvg } from '$app/components/_shared/svg/GroupByFieldSvg';
|
||||||
|
import { HideMenuSvg } from '$app/components/_shared/svg/HideMenuSvg';
|
||||||
|
import { InformationSvg } from '$app/components/_shared/svg/InformationSvg';
|
||||||
|
import { LogoutSvg } from '$app/components/_shared/svg/LogoutSvg';
|
||||||
|
import { MoreSvg } from '$app/components/_shared/svg/MoreSvg';
|
||||||
|
import { MultiSelectTypeSvg } from '$app/components/_shared/svg/MultiSelectTypeSvg';
|
||||||
|
import { NumberTypeSvg } from '$app/components/_shared/svg/NumberTypeSvg';
|
||||||
|
import { PropertiesSvg } from '$app/components/_shared/svg/PropertiesSvg';
|
||||||
|
import { SearchSvg } from '$app/components/_shared/svg/SearchSvg';
|
||||||
|
import { ShowMenuSvg } from '$app/components/_shared/svg/ShowMenuSvg';
|
||||||
|
import { SingleSelectTypeSvg } from '$app/components/_shared/svg/SingleSelectTypeSvg';
|
||||||
|
import { SkipLeftSvg } from '$app/components/_shared/svg/SkipLeftSvg';
|
||||||
|
import { SkipRightSvg } from '$app/components/_shared/svg/SkipRightSvg';
|
||||||
|
import { SortSvg } from '$app/components/_shared/svg/SortSvg';
|
||||||
|
import { TextTypeSvg } from '$app/components/_shared/svg/TextTypeSvg';
|
||||||
|
import { TrashSvg } from '$app/components/_shared/svg/TrashSvg';
|
||||||
|
import { UrlTypeSvg } from '$app/components/_shared/svg/UrlTypeSvg';
|
||||||
|
|
||||||
|
export const AllIcons = () => {
|
||||||
|
return (
|
||||||
|
<div className={'p-8'}>
|
||||||
|
<h1 className={'mb-12 text-2xl'}>Icons</h1>
|
||||||
|
<div className={'mb-8'}>
|
||||||
|
<div className={'flex flex-wrap items-center gap-8'}>
|
||||||
|
<i className={'h-5 w-5'} title={'AddSvg'}>
|
||||||
|
<AddSvg></AddSvg>
|
||||||
|
</i>
|
||||||
|
<i className={'h-5 w-5'} title={'ArrowLeftSvg'}>
|
||||||
|
<ArrowLeftSvg></ArrowLeftSvg>
|
||||||
|
</i>
|
||||||
|
<i className={'h-5 w-5'} title={'ArrowRightSvg'}>
|
||||||
|
<ArrowRightSvg></ArrowRightSvg>
|
||||||
|
</i>
|
||||||
|
<i className={'h-5 w-5'} title={'BoardSvg'}>
|
||||||
|
<BoardSvg></BoardSvg>
|
||||||
|
</i>
|
||||||
|
<i className={'h-5 w-5'} title={'CheckboxSvg'}>
|
||||||
|
<CheckboxSvg></CheckboxSvg>
|
||||||
|
</i>
|
||||||
|
<i className={'h-5 w-5'} title={'ChecklistTypeSvg'}>
|
||||||
|
<ChecklistTypeSvg></ChecklistTypeSvg>
|
||||||
|
</i>
|
||||||
|
<i className={'h-5 w-5'} title={'CheckmarkSvg'}>
|
||||||
|
<CheckmarkSvg></CheckmarkSvg>
|
||||||
|
</i>
|
||||||
|
<i className={'h-5 w-5'} title={'ClockSvg'}>
|
||||||
|
<ClockSvg></ClockSvg>
|
||||||
|
</i>
|
||||||
|
<i className={'h-5 w-5'} title={'CloseSvg'}>
|
||||||
|
<CloseSvg></CloseSvg>
|
||||||
|
</i>
|
||||||
|
<i className={'h-5 w-5'} title={'CopySvg'}>
|
||||||
|
<CopySvg></CopySvg>
|
||||||
|
</i>
|
||||||
|
<i className={'h-5 w-5'} title={'DateTypeSvg'}>
|
||||||
|
<DateTypeSvg></DateTypeSvg>
|
||||||
|
</i>
|
||||||
|
<i className={'h-5 w-5'} title={'Details2Svg'}>
|
||||||
|
<Details2Svg></Details2Svg>
|
||||||
|
</i>
|
||||||
|
<i className={'h-5 w-5'} title={'DocumentSvg'}>
|
||||||
|
<DocumentSvg></DocumentSvg>
|
||||||
|
</i>
|
||||||
|
<i className={'h-5 w-5'} title={'DropDownShowSvg'}>
|
||||||
|
<DropDownShowSvg></DropDownShowSvg>
|
||||||
|
</i>
|
||||||
|
<i className={'h-5 w-5'} title={'EarthSvg'}>
|
||||||
|
<EarthSvg></EarthSvg>
|
||||||
|
</i>
|
||||||
|
<i className={'h-5 w-5'} title={'EditorCheckSvg'}>
|
||||||
|
<EditorCheckSvg></EditorCheckSvg>
|
||||||
|
</i>
|
||||||
|
<i className={'h-5 w-5'} title={'EditorUncheckSvg'}>
|
||||||
|
<EditorUncheckSvg></EditorUncheckSvg>
|
||||||
|
</i>
|
||||||
|
<i className={'h-5 w-5'} title={'EditSvg'}>
|
||||||
|
<EditSvg></EditSvg>
|
||||||
|
</i>
|
||||||
|
<i className={'h-5 w-5'} title={'EyeClosedSvg'}>
|
||||||
|
<EyeClosedSvg></EyeClosedSvg>
|
||||||
|
</i>
|
||||||
|
<i className={'h-5 w-5'} title={'EyeOpenSvg'}>
|
||||||
|
<EyeOpenSvg></EyeOpenSvg>
|
||||||
|
</i>
|
||||||
|
<i className={'h-5 w-5'} title={'FilterSvg'}>
|
||||||
|
<FilterSvg></FilterSvg>
|
||||||
|
</i>
|
||||||
|
<i className={'h-5 w-5'} title={'GridSvg'}>
|
||||||
|
<GridSvg></GridSvg>
|
||||||
|
</i>
|
||||||
|
<i className={'h-5 w-5'} title={'GroupByFieldSvg'}>
|
||||||
|
<GroupByFieldSvg></GroupByFieldSvg>
|
||||||
|
</i>
|
||||||
|
<i className={'h-5 w-5'} title={'HideMenuSvg'}>
|
||||||
|
<HideMenuSvg></HideMenuSvg>
|
||||||
|
</i>
|
||||||
|
<i className={'h-5 w-5'} title={'InformationSvg'}>
|
||||||
|
<InformationSvg></InformationSvg>
|
||||||
|
</i>
|
||||||
|
<i className={'h-5 w-5'} title={'LogoutSvg'}>
|
||||||
|
<LogoutSvg></LogoutSvg>
|
||||||
|
</i>
|
||||||
|
<i className={'h-5 w-5'} title={'MoreSvg'}>
|
||||||
|
<MoreSvg></MoreSvg>
|
||||||
|
</i>
|
||||||
|
<i className={'h-5 w-5'} title={'MultiSelectTypeSvg'}>
|
||||||
|
<MultiSelectTypeSvg></MultiSelectTypeSvg>
|
||||||
|
</i>
|
||||||
|
<i className={'h-5 w-5'} title={'NumberTypeSvg'}>
|
||||||
|
<NumberTypeSvg></NumberTypeSvg>
|
||||||
|
</i>
|
||||||
|
<i className={'h-5 w-5'} title={'PropertiesSvg'}>
|
||||||
|
<PropertiesSvg></PropertiesSvg>
|
||||||
|
</i>
|
||||||
|
<i className={'h-5 w-5'} title={'SearchSvg'}>
|
||||||
|
<SearchSvg></SearchSvg>
|
||||||
|
</i>
|
||||||
|
<i className={'h-5 w-5'} title={'ShowMenuSvg'}>
|
||||||
|
<ShowMenuSvg></ShowMenuSvg>
|
||||||
|
</i>
|
||||||
|
<i className={'h-5 w-5'} title={'SingleSelectTypeSvg'}>
|
||||||
|
<SingleSelectTypeSvg></SingleSelectTypeSvg>
|
||||||
|
</i>
|
||||||
|
<i className={'h-5 w-5'} title={'SkipLeftSvg'}>
|
||||||
|
<SkipLeftSvg></SkipLeftSvg>
|
||||||
|
</i>
|
||||||
|
<i className={'h-5 w-5'} title={'SkipRightSvg'}>
|
||||||
|
<SkipRightSvg></SkipRightSvg>
|
||||||
|
</i>
|
||||||
|
<i className={'h-5 w-5'} title={'SortSvg'}>
|
||||||
|
<SortSvg></SortSvg>
|
||||||
|
</i>
|
||||||
|
<i className={'h-5 w-5'} title={'TextTypeSvg'}>
|
||||||
|
<TextTypeSvg></TextTypeSvg>
|
||||||
|
</i>
|
||||||
|
<i className={'h-5 w-5'} title={'TrashSvg'}>
|
||||||
|
<TrashSvg></TrashSvg>
|
||||||
|
</i>
|
||||||
|
<i className={'h-5 w-5'} title={'UrlTypeSvg'}>
|
||||||
|
<UrlTypeSvg></UrlTypeSvg>
|
||||||
|
</i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,45 @@
|
|||||||
|
export const ColorPalette = () => {
|
||||||
|
return (
|
||||||
|
<div className={'p-8'}>
|
||||||
|
<h1 className={'mb-4 text-2xl'}>Colors</h1>
|
||||||
|
<h2 className={'mb-4'}>Main</h2>
|
||||||
|
<div className={'mb-8 flex flex-wrap items-center'}>
|
||||||
|
<div title={'main-accent'} className={'m-2 h-[100px] w-[100px] bg-main-accent'}></div>
|
||||||
|
<div title={'main-hovered'} className={'m-2 h-[100px] w-[100px] bg-main-hovered'}></div>
|
||||||
|
<div title={'main-secondary'} className={'m-2 h-[100px] w-[100px] bg-main-secondary'}></div>
|
||||||
|
<div title={'main-selector'} className={'m-2 h-[100px] w-[100px] bg-main-selector'}></div>
|
||||||
|
<div title={'main-alert'} className={'m-2 h-[100px] w-[100px] bg-main-alert'}></div>
|
||||||
|
<div title={'main-warning'} className={'m-2 h-[100px] w-[100px] bg-main-warning'}></div>
|
||||||
|
<div title={'main-success'} className={'m-2 h-[100px] w-[100px] bg-main-success'}></div>
|
||||||
|
</div>
|
||||||
|
<h2 className={'mb-4'}>Tint</h2>
|
||||||
|
<div className={'mb-8 flex flex-wrap items-center'}>
|
||||||
|
<div title={'tint-1'} className={'m-2 h-[100px] w-[100px] bg-tint-1'}></div>
|
||||||
|
<div title={'tint-2'} className={'m-2 h-[100px] w-[100px] bg-tint-2'}></div>
|
||||||
|
<div title={'tint-3'} className={'m-2 h-[100px] w-[100px] bg-tint-3'}></div>
|
||||||
|
<div title={'tint-4'} className={'m-2 h-[100px] w-[100px] bg-tint-4'}></div>
|
||||||
|
<div title={'tint-5'} className={'m-2 h-[100px] w-[100px] bg-tint-5'}></div>
|
||||||
|
<div title={'tint-6'} className={'m-2 h-[100px] w-[100px] bg-tint-6'}></div>
|
||||||
|
<div title={'tint-7'} className={'m-2 h-[100px] w-[100px] bg-tint-7'}></div>
|
||||||
|
<div title={'tint-8'} className={'m-2 h-[100px] w-[100px] bg-tint-8'}></div>
|
||||||
|
<div title={'tint-9'} className={'m-2 h-[100px] w-[100px] bg-tint-9'}></div>
|
||||||
|
</div>
|
||||||
|
<h2 className={'mb-4'}>Shades</h2>
|
||||||
|
<div className={'mb-8 flex flex-wrap items-center'}>
|
||||||
|
<div title={'shade-1'} className={'m-2 h-[100px] w-[100px] bg-shade-1'}></div>
|
||||||
|
<div title={'shade-2'} className={'m-2 h-[100px] w-[100px] bg-shade-2'}></div>
|
||||||
|
<div title={'shade-3'} className={'m-2 h-[100px] w-[100px] bg-shade-3'}></div>
|
||||||
|
<div title={'shade-4'} className={'m-2 h-[100px] w-[100px] bg-shade-4'}></div>
|
||||||
|
<div title={'shade-5'} className={'m-2 h-[100px] w-[100px] bg-shade-5'}></div>
|
||||||
|
<div title={'shade-6'} className={'m-2 h-[100px] w-[100px] bg-shade-6'}></div>
|
||||||
|
</div>
|
||||||
|
<h2 className={'mb-4'}>Surface</h2>
|
||||||
|
<div className={'mb-8 flex flex-wrap items-center'}>
|
||||||
|
<div title={'surface-1'} className={'m-2 h-[100px] w-[100px] bg-surface-1'}></div>
|
||||||
|
<div title={'surface-2'} className={'m-2 h-[100px] w-[100px] bg-surface-2'}></div>
|
||||||
|
<div title={'surface-3'} className={'m-2 h-[100px] w-[100px] bg-surface-3'}></div>
|
||||||
|
<div title={'surface-4'} className={'bg-surface-4 m-2 h-[100px] w-[100px]'}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -8,7 +8,7 @@ async function testCreateDocument() {
|
|||||||
const document = await svc.open().then((result) => result.unwrap());
|
const document = await svc.open().then((result) => result.unwrap());
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const content = JSON.parse(document.content);
|
// const content = JSON.parse(document.content);
|
||||||
// The initial document content:
|
// The initial document content:
|
||||||
// {
|
// {
|
||||||
// "document": {
|
// "document": {
|
||||||
|
@ -12,14 +12,21 @@ export enum BlockType {
|
|||||||
TableBlock = 'table',
|
TableBlock = 'table',
|
||||||
ColumnBlock = 'column',
|
ColumnBlock = 'column',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface HeadingBlockData {
|
||||||
|
level: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TextBlockData {
|
||||||
|
delta: TextDelta[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PageBlockData extends TextBlockData {}
|
||||||
|
|
||||||
export interface NestedBlock {
|
export interface NestedBlock {
|
||||||
id: string;
|
id: string;
|
||||||
type: BlockType;
|
type: BlockType;
|
||||||
data: {
|
data: Record<string, any>;
|
||||||
delta?: TextDelta[];
|
|
||||||
};
|
|
||||||
externalId: string;
|
|
||||||
externalType: 'text' | 'array' | 'map';
|
|
||||||
parent: string | null;
|
parent: string | null;
|
||||||
children: string;
|
children: string;
|
||||||
}
|
}
|
||||||
|
@ -65,7 +65,7 @@ export class DatabaseController {
|
|||||||
this.databaseViewCache.initializeWithRows(database.rows);
|
this.databaseViewCache.initializeWithRows(database.rows);
|
||||||
|
|
||||||
this._callback?.onViewChanged?.(database);
|
this._callback?.onViewChanged?.(database);
|
||||||
return loadGroupResult;
|
return Ok(database.rows);
|
||||||
} else {
|
} else {
|
||||||
return Err(openDatabaseResult.val);
|
return Err(openDatabaseResult.val);
|
||||||
}
|
}
|
||||||
|
@ -4,27 +4,45 @@ import {
|
|||||||
EditPayloadPB,
|
EditPayloadPB,
|
||||||
FlowyError,
|
FlowyError,
|
||||||
OpenDocumentPayloadPB,
|
OpenDocumentPayloadPB,
|
||||||
|
DocumentDataPB2,
|
||||||
ViewIdPB,
|
ViewIdPB,
|
||||||
|
OpenDocumentPayloadPBV2,
|
||||||
|
ApplyActionPayloadPBV2,
|
||||||
|
BlockActionTypePB,
|
||||||
|
BlockActionPB,
|
||||||
|
CloseDocumentPayloadPBV2,
|
||||||
} from '@/services/backend';
|
} from '@/services/backend';
|
||||||
import { DocumentEventApplyEdit, DocumentEventGetDocument } from '@/services/backend/events/flowy-document';
|
import { DocumentEventApplyEdit, DocumentEventGetDocument } from '@/services/backend/events/flowy-document';
|
||||||
import { Result } from 'ts-results';
|
import { Result } from 'ts-results';
|
||||||
import { FolderEventCloseView } from '@/services/backend/events/flowy-folder2';
|
import { FolderEventCloseView } from '@/services/backend/events/flowy-folder2';
|
||||||
|
import {
|
||||||
|
DocumentEvent2ApplyAction,
|
||||||
|
DocumentEvent2CloseDocument,
|
||||||
|
DocumentEvent2OpenDocument,
|
||||||
|
} from '@/services/backend/events/flowy-document2';
|
||||||
|
|
||||||
export class DocumentBackendService {
|
export class DocumentBackendService {
|
||||||
constructor(public readonly viewId: string) {}
|
constructor(public readonly viewId: string) {}
|
||||||
|
|
||||||
open = (): Promise<Result<DocumentDataPB, FlowyError>> => {
|
open = (): Promise<Result<DocumentDataPB2, FlowyError>> => {
|
||||||
const payload = OpenDocumentPayloadPB.fromObject({ document_id: this.viewId, version: DocumentVersionPB.V1 });
|
const payload = OpenDocumentPayloadPBV2.fromObject({
|
||||||
return DocumentEventGetDocument(payload);
|
document_id: this.viewId,
|
||||||
|
});
|
||||||
|
return DocumentEvent2OpenDocument(payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
applyEdit = (operations: string) => {
|
applyActions = (actions: [BlockActionPB]): Promise<Result<void, FlowyError>> => {
|
||||||
const payload = EditPayloadPB.fromObject({ doc_id: this.viewId, operations: operations });
|
const payload = ApplyActionPayloadPBV2.fromObject({
|
||||||
return DocumentEventApplyEdit(payload);
|
document_id: this.viewId,
|
||||||
|
actions: actions,
|
||||||
|
});
|
||||||
|
return DocumentEvent2ApplyAction(payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
close = () => {
|
close = (): Promise<Result<void, FlowyError>> => {
|
||||||
const payload = ViewIdPB.fromObject({ value: this.viewId });
|
const payload = CloseDocumentPayloadPBV2.fromObject({
|
||||||
return FolderEventCloseView(payload);
|
document_id: this.viewId,
|
||||||
|
});
|
||||||
|
return DocumentEvent2CloseDocument(payload);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,29 +1,53 @@
|
|||||||
import { DocumentData, BlockType, TextDelta } from '@/appflowy_app/interfaces/document';
|
import { DocumentData, BlockType } from '@/appflowy_app/interfaces/document';
|
||||||
import { createContext } from 'react';
|
import { createContext } from 'react';
|
||||||
import { DocumentBackendService } from './document_bd_svc';
|
import { DocumentBackendService } from './document_bd_svc';
|
||||||
|
import { FlowyError } from '@/services/backend';
|
||||||
|
import { DocumentObserver } from './document_observer';
|
||||||
|
|
||||||
export const DocumentControllerContext = createContext<DocumentController | null>(null);
|
export const DocumentControllerContext = createContext<DocumentController | null>(null);
|
||||||
|
|
||||||
export class DocumentController {
|
export class DocumentController {
|
||||||
private readonly backendService: DocumentBackendService;
|
private readonly backendService: DocumentBackendService;
|
||||||
|
private readonly observer: DocumentObserver;
|
||||||
|
|
||||||
constructor(public readonly viewId: string) {
|
constructor(public readonly viewId: string) {
|
||||||
this.backendService = new DocumentBackendService(viewId);
|
this.backendService = new DocumentBackendService(viewId);
|
||||||
|
this.observer = new DocumentObserver(viewId);
|
||||||
}
|
}
|
||||||
|
|
||||||
open = async (): Promise<DocumentData | null> => {
|
open = async (): Promise<DocumentData | FlowyError> => {
|
||||||
const openDocumentResult = await this.backendService.open();
|
// example:
|
||||||
if (openDocumentResult.ok) {
|
await this.observer.subscribe({
|
||||||
|
didReceiveUpdate: () => {
|
||||||
|
console.log('didReceiveUpdate');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const document = await this.backendService.open();
|
||||||
|
if (document.ok) {
|
||||||
|
console.log(document.val);
|
||||||
|
const blocks: DocumentData["blocks"] = {};
|
||||||
|
document.val.blocks.forEach((block) => {
|
||||||
|
blocks[block.id] = {
|
||||||
|
id: block.id,
|
||||||
|
type: block.ty as BlockType,
|
||||||
|
parent: block.parent_id,
|
||||||
|
children: block.children_id,
|
||||||
|
data: JSON.parse(block.data),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const childrenMap: Record<string, string[]> = {};
|
||||||
|
document.val.meta.children_map.forEach((child, key) => { childrenMap[key] = child.children; });
|
||||||
return {
|
return {
|
||||||
rootId: '',
|
rootId: document.val.page_id,
|
||||||
blocks: {},
|
blocks,
|
||||||
meta: {
|
meta: {
|
||||||
childrenMap: {},
|
childrenMap
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
return document.val;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
applyActions = (
|
applyActions = (
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
import { Ok, Result } from 'ts-results';
|
||||||
|
import { ChangeNotifier } from '$app/utils/change_notifier';
|
||||||
|
import { FolderNotificationObserver } from '../folder/notifications/observer';
|
||||||
|
import { DocumentNotification } from '@/services/backend';
|
||||||
|
import { DocumentNotificationObserver } from './notifications/observer';
|
||||||
|
|
||||||
|
export type DidReceiveUpdateCallback = () => void; // todo: add params
|
||||||
|
|
||||||
|
export class DocumentObserver {
|
||||||
|
private listener?: DocumentNotificationObserver;
|
||||||
|
|
||||||
|
constructor(public readonly workspaceId: string) {}
|
||||||
|
|
||||||
|
subscribe = async (callbacks: { didReceiveUpdate: DidReceiveUpdateCallback }) => {
|
||||||
|
this.listener = new DocumentNotificationObserver({
|
||||||
|
viewId: this.workspaceId,
|
||||||
|
parserHandler: (notification, result) => {
|
||||||
|
switch (notification) {
|
||||||
|
case DocumentNotification.DidReceiveUpdate:
|
||||||
|
callbacks.didReceiveUpdate();
|
||||||
|
// Fixme: ...
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await this.listener.start();
|
||||||
|
};
|
||||||
|
|
||||||
|
unsubscribe = async () => {
|
||||||
|
// this.appListNotifier.unsubscribe();
|
||||||
|
// this.workspaceNotifier.unsubscribe();
|
||||||
|
await this.listener?.stop();
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
import { OnNotificationError, AFNotificationObserver } from '@/services/backend/notifications';
|
||||||
|
import { DocumentNotificationParser } from './parser';
|
||||||
|
import { FlowyError, DocumentNotification } from '@/services/backend';
|
||||||
|
import { Result } from 'ts-results';
|
||||||
|
|
||||||
|
export type ParserHandler = (notification: DocumentNotification, payload: Result<Uint8Array, FlowyError>) => void;
|
||||||
|
|
||||||
|
export class DocumentNotificationObserver extends AFNotificationObserver<DocumentNotification> {
|
||||||
|
constructor(params: { viewId?: string; parserHandler: ParserHandler; onError?: OnNotificationError }) {
|
||||||
|
const parser = new DocumentNotificationParser({
|
||||||
|
callback: params.parserHandler,
|
||||||
|
id: params.viewId,
|
||||||
|
});
|
||||||
|
super(parser);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
import { NotificationParser, OnNotificationError } from '@/services/backend/notifications';
|
||||||
|
import { FlowyError, DocumentNotification } from '@/services/backend';
|
||||||
|
import { Result } from 'ts-results';
|
||||||
|
|
||||||
|
declare type DocumentNotificationCallback = (ty: DocumentNotification, payload: Result<Uint8Array, FlowyError>) => void;
|
||||||
|
|
||||||
|
export class DocumentNotificationParser extends NotificationParser<DocumentNotification> {
|
||||||
|
constructor(params: { id?: string; callback: DocumentNotificationCallback; onError?: OnNotificationError }) {
|
||||||
|
super(
|
||||||
|
params.callback,
|
||||||
|
(ty) => {
|
||||||
|
const notification = DocumentNotification[ty];
|
||||||
|
if (isDocumentNotification(notification)) {
|
||||||
|
return DocumentNotification[notification];
|
||||||
|
} else {
|
||||||
|
return DocumentNotification.Unknown;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
params.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDocumentNotification = (notification: string): notification is keyof typeof DocumentNotification => {
|
||||||
|
return Object.values(DocumentNotification).indexOf(notification) !== -1;
|
||||||
|
};
|
@ -1,22 +1,24 @@
|
|||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Board } from '../components/board/Board';
|
import { Board } from '../components/board/Board';
|
||||||
|
import { useAppSelector } from '$app/stores/store';
|
||||||
|
|
||||||
export const BoardPage = () => {
|
export const BoardPage = () => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const [viewId, setViewId] = useState('');
|
const [viewId, setViewId] = useState('');
|
||||||
|
const pagesStore = useAppSelector((state) => state.pages);
|
||||||
|
const [title, setTitle] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (params?.id?.length) {
|
if (params?.id?.length) {
|
||||||
setViewId(params.id);
|
setViewId(params.id);
|
||||||
// setDatabaseId('testDb');
|
setTitle(pagesStore.find((page) => page.id === params.id)?.title || '');
|
||||||
}
|
}
|
||||||
}, [params]);
|
}, [params, pagesStore]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex h-full flex-col gap-8 px-8 pt-8'>
|
<div className='flex h-full flex-col gap-8 px-8 pt-8'>
|
||||||
<h1 className='text-4xl font-bold'>Board: {viewId}</h1>
|
{viewId?.length && <Board viewId={viewId} title={title} />}
|
||||||
{viewId?.length && <Board viewId={viewId} />}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -23,7 +23,7 @@ export const useDocument = () => {
|
|||||||
const res = await c.open();
|
const res = await c.open();
|
||||||
console.log(res)
|
console.log(res)
|
||||||
if (!res) return;
|
if (!res) return;
|
||||||
setDocumentData(res)
|
// setDocumentData(res)
|
||||||
setDocumentId(params.id)
|
setDocumentId(params.id)
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
export const useGrid = () => {
|
|
||||||
const loadGrid = async (id: string) => {
|
|
||||||
console.log('loading grid');
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
loadGrid,
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,45 +1,22 @@
|
|||||||
import { GridAddView } from '../components/grid/GridAddView/GridAddView';
|
|
||||||
import { GridTableCount } from '../components/grid/GridTableCount/GridTableCount';
|
|
||||||
import { GridTableHeader } from '../components/grid/GridTableHeader/GridTableHeader';
|
|
||||||
import { GridAddRow } from '../components/grid/GridTableRows/GridAddRow';
|
|
||||||
import { GridTableRows } from '../components/grid/GridTableRows/GridTableRows';
|
|
||||||
import { GridTitle } from '../components/grid/GridTitle/GridTitle';
|
|
||||||
import { SearchInput } from '../components/_shared/SearchInput';
|
|
||||||
import { GridToolbar } from '../components/grid/GridToolbar/GridToolbar';
|
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { useGrid } from './GridPage.hooks';
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Grid } from '../components/grid/Grid/Grid';
|
||||||
|
|
||||||
export const GridPage = () => {
|
export const GridPage = () => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const { loadGrid } = useGrid();
|
const [viewId, setViewId] = useState('');
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void (async () => {
|
if (params?.id?.length) {
|
||||||
if (!params?.id) return;
|
setViewId(params.id);
|
||||||
await loadGrid(params.id);
|
// setDatabaseId('testDb');
|
||||||
})();
|
}
|
||||||
}, [params]);
|
}, [params]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mx-auto mt-8 flex flex-col gap-8 px-8'>
|
<div className='flex h-full flex-col gap-8 px-8 pt-8'>
|
||||||
<h1 className='text-4xl font-bold'>Grid</h1>
|
<h1 className='text-4xl font-bold'>Grid: {viewId}</h1>
|
||||||
|
{viewId?.length && <Grid viewId={viewId} />}
|
||||||
<div className='flex w-full items-center justify-between'>
|
|
||||||
<GridTitle />
|
|
||||||
<GridToolbar />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* table component view with text area for td */}
|
|
||||||
<div className='flex flex-col gap-4'>
|
|
||||||
<table className='w-full table-fixed text-sm'>
|
|
||||||
<GridTableHeader />
|
|
||||||
<GridTableRows />
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<GridAddRow />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<GridTableCount />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -2,5 +2,6 @@ export * from "./models/flowy-user";
|
|||||||
export * from "./models/flowy-document";
|
export * from "./models/flowy-document";
|
||||||
export * from "./models/flowy-database";
|
export * from "./models/flowy-database";
|
||||||
export * from "./models/flowy-folder2";
|
export * from "./models/flowy-folder2";
|
||||||
|
export * from "./models/flowy-document2";
|
||||||
export * from "./models/flowy-net";
|
export * from "./models/flowy-net";
|
||||||
export * from "./models/flowy-error";
|
export * from "./models/flowy-error";
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user