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/user/application/user_service.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_backend/protobuf/flowy-document/entities.pb.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 'dart:async';
|
||||
import 'package:appflowy/util/either_extension.dart';
|
||||
|
||||
import 'package:appflowy_backend/protobuf/flowy-document2/entities.pb.dart';
|
||||
part 'doc_bloc.freezed.dart';
|
||||
|
||||
class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
final ViewPB view;
|
||||
final DocumentService _documentService;
|
||||
final DocumentListener _docListener;
|
||||
|
||||
final ViewListener _listener;
|
||||
final TrashService _trashService;
|
||||
@ -32,12 +34,14 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
DocumentBloc({
|
||||
required this.view,
|
||||
}) : _documentService = DocumentService(),
|
||||
_docListener = DocumentListener(id: view.id),
|
||||
_listener = ViewListener(view: view),
|
||||
_trashService = TrashService(),
|
||||
super(DocumentState.initial()) {
|
||||
on<DocumentEvent>((event, emit) async {
|
||||
await event.map(
|
||||
initial: (Initial value) async {
|
||||
_listenOnDocChange();
|
||||
await _initial(value, emit);
|
||||
_listenOnViewChange();
|
||||
},
|
||||
@ -73,6 +77,7 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
}
|
||||
|
||||
await _documentService.closeDocument(docId: view.id);
|
||||
await _documentService.closeDocumentV2(view: view);
|
||||
return super.close();
|
||||
}
|
||||
|
||||
@ -88,6 +93,39 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
);
|
||||
}
|
||||
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(
|
||||
(documentData) async {
|
||||
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 {
|
||||
final document = Document.fromJson(jsonDecode(documentData.content));
|
||||
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-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document2/entities.pb.dart';
|
||||
|
||||
class DocumentService {
|
||||
Future<Either<DocumentDataPB, FlowyError>> openDocument({
|
||||
@ -39,4 +40,32 @@ class DocumentService {
|
||||
final payload = ViewIdPB(value: docId);
|
||||
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/flowy-folder2/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';
|
||||
|
||||
// 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-database/dart_event.dart';
|
||||
part 'dart_event/flowy-document/dart_event.dart';
|
||||
part 'dart_event/flowy-document2/dart_event.dart';
|
||||
|
||||
enum FFIException {
|
||||
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-folder = { 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-folder = { path = "../../AppFlowy-Collab/collab-folder" }
|
||||
#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 { TestColors } from './components/TestColors/TestColors';
|
||||
import { ColorPalette } from './components/tests/ColorPalette';
|
||||
import { Provider } from 'react-redux';
|
||||
import { store } from './stores/store';
|
||||
import { DocumentPage } from './views/DocumentPage';
|
||||
@ -14,6 +14,8 @@ import { ErrorHandlerPage } from './components/error/ErrorHandlerPage';
|
||||
import initializeI18n from './stores/i18n/initializeI18n';
|
||||
import { TestAPI } from './components/tests/TestAPI';
|
||||
import { GetStarted } from './components/auth/GetStarted/GetStarted';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { AllIcons } from '$app/components/tests/AllIcons';
|
||||
|
||||
initializeI18n();
|
||||
|
||||
@ -21,9 +23,11 @@ const App = () => {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Provider store={store}>
|
||||
<ErrorBoundary FallbackComponent={ErrorHandlerPage}>
|
||||
<Routes>
|
||||
<Route path={'/'} element={<ProtectedRoutes />}>
|
||||
<Route path={'/page/colors'} element={<TestColors />} />
|
||||
<Route path={'/page/all-icons'} element={<AllIcons />} />
|
||||
<Route path={'/page/colors'} element={<ColorPalette />} />
|
||||
<Route path={'/page/api-test'} element={<TestAPI />} />
|
||||
<Route path={'/page/document/:id'} element={<DocumentPage />} />
|
||||
<Route path={'/page/board/:id'} element={<BoardPage />} />
|
||||
@ -34,7 +38,7 @@ const App = () => {
|
||||
<Route path={'/auth/signUp'} element={<SignUpPage />}></Route>
|
||||
<Route path={'/auth/confirm-account'} element={<ConfirmAccountPage />}></Route>
|
||||
</Routes>
|
||||
<ErrorHandlerPage></ErrorHandlerPage>
|
||||
</ErrorBoundary>
|
||||
</Provider>
|
||||
</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
|
||||
ref={ref}
|
||||
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) => (
|
||||
<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 { CheckmarkSvg } from '$app/components/_shared/svg/CheckmarkSvg';
|
||||
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 { 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 = ({
|
||||
top,
|
||||
@ -21,6 +21,7 @@ export const CellOptionsPopup = ({
|
||||
cellCache,
|
||||
fieldController,
|
||||
onOutsideClick,
|
||||
openOptionDetail,
|
||||
}: {
|
||||
top: number;
|
||||
left: number;
|
||||
@ -28,27 +29,19 @@ export const CellOptionsPopup = ({
|
||||
cellCache: CellCache;
|
||||
fieldController: FieldController;
|
||||
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 [adjustedTop, setAdjustedTop] = useState(-100);
|
||||
const [value, setValue] = useState('');
|
||||
const { data, cellController } = useCell(cellIdentifier, cellCache, fieldController);
|
||||
const { data } = useCell(cellIdentifier, cellCache, fieldController);
|
||||
const databaseStore = useAppSelector((state) => state.database);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) return;
|
||||
const { height } = ref.current.getBoundingClientRect();
|
||||
if (top + height + 40 > window.innerHeight) {
|
||||
setAdjustedTop(window.innerHeight - height - 40);
|
||||
} else {
|
||||
setAdjustedTop(top);
|
||||
if (inputRef?.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [ref, window, top, left]);
|
||||
|
||||
useOutsideClick(ref, async () => {
|
||||
onOutsideClick();
|
||||
});
|
||||
}, [inputRef]);
|
||||
|
||||
const onKeyDown: KeyboardEventHandler = async (e) => {
|
||||
if (e.key === 'Enter' && value.length > 0) {
|
||||
@ -63,11 +56,7 @@ export const CellOptionsPopup = ({
|
||||
};
|
||||
|
||||
const onToggleOptionClick = async (option: SelectOptionPB) => {
|
||||
if (
|
||||
(data as SelectOptionCellDataPB | undefined)?.select_options?.find(
|
||||
(selectedOption) => selectedOption.id === option.id
|
||||
)
|
||||
) {
|
||||
if ((data as SelectOptionCellDataPB)?.select_options?.find((selectedOption) => selectedOption.id === option.id)) {
|
||||
await new SelectOptionCellBackendService(cellIdentifier).unselectOption([option.id]);
|
||||
} else {
|
||||
await new SelectOptionCellBackendService(cellIdentifier).selectOption([option.id]);
|
||||
@ -75,23 +64,37 @@ export const CellOptionsPopup = ({
|
||||
setValue('');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
console.log('loaded data: ', data);
|
||||
console.log('have stored ', databaseStore.fields[cellIdentifier.fieldId]);
|
||||
}, [data]);
|
||||
const onKeyDownWrapper: KeyboardEventHandler = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
onOutsideClick();
|
||||
}
|
||||
};
|
||||
|
||||
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 (
|
||||
<div
|
||||
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={'flex flex-col gap-2 p-2'}>
|
||||
<PopupWindow className={'p-2 text-xs'} onOutsideClick={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 '}>
|
||||
<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}>
|
||||
<span>{option?.name || ''}</span>
|
||||
<button onClick={() => onUnselectOptionClick(option)} className={'h-5 w-5 cursor-pointer'}>
|
||||
@ -101,6 +104,7 @@ export const CellOptionsPopup = ({
|
||||
)) || ''}
|
||||
</div>
|
||||
<input
|
||||
ref={inputRef}
|
||||
className={'py-2'}
|
||||
value={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>
|
||||
<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'}>
|
||||
{(databaseStore.fields[cellIdentifier.fieldId]?.fieldOptions as ISelectOptionType).selectOptions.map(
|
||||
(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={'flex items-center'}>
|
||||
{(data as SelectOptionCellDataPB | undefined)?.select_options?.find(
|
||||
{(data as SelectOptionCellDataPB)?.select_options?.find(
|
||||
(selectedOption) => selectedOption.id === option.selectOptionId
|
||||
) && (
|
||||
<button className={'h-5 w-5 p-1'}>
|
||||
<CheckmarkSvg></CheckmarkSvg>
|
||||
</button>
|
||||
)}
|
||||
<button className={'h-6 w-6 p-1'}>
|
||||
<button onClick={(e) => onOptionDetailClick(e, option)} className={'h-6 w-6 p-1'}>
|
||||
<Details2Svg></Details2Svg>
|
||||
</button>
|
||||
</div>
|
||||
@ -147,6 +151,6 @@ export const CellOptionsPopup = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PopupWindow>
|
||||
);
|
||||
};
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { FieldType } from '@/services/backend';
|
||||
import { FieldTypeIcon } from '$app/components/_shared/EditRow/FieldTypeIcon';
|
||||
import { FieldTypeName } from '$app/components/_shared/EditRow/FieldTypeName';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import useOutsideClick from '$app/components/_shared/useOutsideClick';
|
||||
import { PopupWindow } from '$app/components/_shared/PopupWindow';
|
||||
|
||||
const typesOrder: FieldType[] = [
|
||||
FieldType.RichText,
|
||||
@ -17,39 +16,17 @@ const typesOrder: FieldType[] = [
|
||||
|
||||
export const ChangeFieldTypePopup = ({
|
||||
top,
|
||||
right,
|
||||
left,
|
||||
onClick,
|
||||
onOutsideClick,
|
||||
}: {
|
||||
top: number;
|
||||
right: number;
|
||||
left: number;
|
||||
onClick: (newType: FieldType) => 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 (
|
||||
<div
|
||||
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` }}
|
||||
>
|
||||
<PopupWindow className={'p-2 text-xs'} onOutsideClick={onOutsideClick} left={left} top={top}>
|
||||
<div className={'flex flex-col'}>
|
||||
{typesOrder.map((t, i) => (
|
||||
<button
|
||||
@ -66,6 +43,6 @@ export const ChangeFieldTypePopup = ({
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</PopupWindow>
|
||||
);
|
||||
};
|
||||
|
@ -1,15 +1,17 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
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 useOutsideClick from '$app/components/_shared/useOutsideClick';
|
||||
import Calendar from 'react-calendar';
|
||||
import dayjs from 'dayjs';
|
||||
import { ClockSvg } from '$app/components/_shared/svg/ClockSvg';
|
||||
import { MoreSvg } from '$app/components/_shared/svg/MoreSvg';
|
||||
import { EditorUncheckSvg } from '$app/components/_shared/svg/EditorUncheckSvg';
|
||||
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 = ({
|
||||
left,
|
||||
@ -27,47 +29,27 @@ export const DatePickerPopup = ({
|
||||
onOutsideClick: () => void;
|
||||
}) => {
|
||||
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 [selectedDate, setSelectedDate] = useState<Date>(new Date());
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) return;
|
||||
const { height } = ref.current.getBoundingClientRect();
|
||||
if (top + height + 40 > window.innerHeight) {
|
||||
setAdjustedTop(top - height - 40);
|
||||
} else {
|
||||
setAdjustedTop(top);
|
||||
}
|
||||
}, [ref, window, top, left]);
|
||||
const date_pb = data as DateCellDataPB | undefined;
|
||||
if (!date_pb || !date_pb?.date.length) return;
|
||||
|
||||
useOutsideClick(ref, async () => {
|
||||
onOutsideClick();
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// console.log((data as DateCellDataPB).date);
|
||||
// setSelectedDate(new Date((data as DateCellDataPB).date));
|
||||
// should be changed after we can modify date format
|
||||
setSelectedDate(dayjs(date_pb.date, 'MMM DD, YYYY').toDate());
|
||||
}, [data]);
|
||||
|
||||
const onChange = (v: Date | null | (Date | null)[]) => {
|
||||
const onChange = async (v: Date | null | (Date | null)[]) => {
|
||||
if (v instanceof Date) {
|
||||
console.log(dayjs(v).format('YYYY-MM-DD'));
|
||||
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 (
|
||||
<div
|
||||
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` }}
|
||||
>
|
||||
<PopupWindow className={'p-2 text-xs'} onOutsideClick={onOutsideClick} left={left} top={top}>
|
||||
<div className={'px-2'}>
|
||||
<Calendar onChange={(d) => onChange(d)} value={selectedDate} />
|
||||
</div>
|
||||
@ -92,6 +74,6 @@ export const DatePickerPopup = ({
|
||||
<MoreSvg></MoreSvg>
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
</PopupWindow>
|
||||
);
|
||||
};
|
||||
|
@ -17,7 +17,7 @@ export const EditCellDate = ({
|
||||
};
|
||||
|
||||
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 || <> </>}
|
||||
</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;
|
||||
cellCache: CellCache;
|
||||
fieldController: FieldController;
|
||||
onEditFieldClick: (top: number, right: number) => void;
|
||||
onEditOptionsClick: (left: number, top: number) => void;
|
||||
onEditDateClick: (left: number, top: number) => void;
|
||||
onEditFieldClick: (cell: CellIdentifier, left: number, top: number) => void;
|
||||
onEditOptionsClick: (cell: CellIdentifier, left: number, top: number) => void;
|
||||
onEditDateClick: (cell: CellIdentifier, left: number, top: number) => void;
|
||||
}) => {
|
||||
const { data, cellController } = useCell(cellIdentifier, cellCache, fieldController);
|
||||
const databaseStore = useAppSelector((state) => state.database);
|
||||
@ -38,7 +38,7 @@ export const EditCellWrapper = ({
|
||||
const onClick = () => {
|
||||
if (!el.current) return;
|
||||
const { top, right } = el.current.getBoundingClientRect();
|
||||
onEditFieldClick(top, right);
|
||||
onEditFieldClick(cellIdentifier, right, top);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -70,17 +70,23 @@ export const EditCellWrapper = ({
|
||||
cellIdentifier.fieldType === FieldType.Checklist) &&
|
||||
cellController && (
|
||||
<CellOptions
|
||||
data={data as SelectOptionCellDataPB | undefined}
|
||||
onEditClick={onEditOptionsClick}
|
||||
data={data as SelectOptionCellDataPB}
|
||||
onEditClick={(left, top) => onEditOptionsClick(cellIdentifier, left, top)}
|
||||
></CellOptions>
|
||||
)}
|
||||
|
||||
{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 && (
|
||||
<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 && (
|
||||
@ -88,7 +94,7 @@ export const EditCellWrapper = ({
|
||||
)}
|
||||
|
||||
{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 && (
|
||||
|
@ -1,22 +1,26 @@
|
||||
import { EditorCheckSvg } from '$app/components/_shared/svg/EditorCheckSvg';
|
||||
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 = ({
|
||||
data,
|
||||
cellController,
|
||||
}: {
|
||||
data: boolean | undefined;
|
||||
cellController: CellController<any, any>;
|
||||
data: 'Yes' | 'No' | undefined;
|
||||
cellController: CheckboxCellController;
|
||||
}) => {
|
||||
const toggleValue = async () => {
|
||||
await cellController?.saveCellData(!data);
|
||||
if (data === 'Yes') {
|
||||
await cellController?.saveCellData('No');
|
||||
} else {
|
||||
await cellController?.saveCellData('Yes');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div onClick={() => toggleValue()} className={'block px-4 py-2'}>
|
||||
<button className={'h-5 w-5'}>
|
||||
{data ? <EditorCheckSvg></EditorCheckSvg> : <EditorUncheckSvg></EditorUncheckSvg>}
|
||||
{data === 'Yes' ? <EditorCheckSvg></EditorCheckSvg> : <EditorUncheckSvg></EditorUncheckSvg>}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import useOutsideClick from '$app/components/_shared/useOutsideClick';
|
||||
import { TrashSvg } from '$app/components/_shared/svg/TrashSvg';
|
||||
import { FieldTypeIcon } from '$app/components/_shared/EditRow/FieldTypeIcon';
|
||||
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 { useAppSelector } from '$app/stores/store';
|
||||
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
|
||||
import { PopupWindow } from '$app/components/_shared/PopupWindow';
|
||||
|
||||
export const EditFieldPopup = ({
|
||||
top,
|
||||
right,
|
||||
left,
|
||||
cellIdentifier,
|
||||
viewId,
|
||||
onOutsideClick,
|
||||
@ -21,7 +21,7 @@ export const EditFieldPopup = ({
|
||||
changeFieldTypeClick,
|
||||
}: {
|
||||
top: number;
|
||||
right: number;
|
||||
left: number;
|
||||
cellIdentifier: CellIdentifier;
|
||||
viewId: string;
|
||||
onOutsideClick: () => void;
|
||||
@ -30,31 +30,13 @@ export const EditFieldPopup = ({
|
||||
}) => {
|
||||
const databaseStore = useAppSelector((state) => state.database);
|
||||
const { t } = useTranslation('');
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const changeTypeButtonRef = useRef<HTMLDivElement>(null);
|
||||
const [name, setName] = useState('');
|
||||
|
||||
const [adjustedTop, setAdjustedTop] = useState(-100);
|
||||
|
||||
useOutsideClick(ref, async () => {
|
||||
await save();
|
||||
onOutsideClick();
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setName(databaseStore.fields[cellIdentifier.fieldId].title);
|
||||
}, [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 () => {
|
||||
if (!fieldInfo) return;
|
||||
const controller = new TypeOptionController(viewId, Some(fieldInfo));
|
||||
@ -78,12 +60,14 @@ export const EditFieldPopup = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
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}px`, left: `${right + 10}px` }}
|
||||
<PopupWindow
|
||||
className={'px-2 py-2 text-xs'}
|
||||
onOutsideClick={async () => {
|
||||
await save();
|
||||
onOutsideClick();
|
||||
}}
|
||||
left={left}
|
||||
top={top}
|
||||
>
|
||||
<div className={'flex flex-col gap-2 p-2'}>
|
||||
<input
|
||||
@ -125,6 +109,6 @@ export const EditFieldPopup = ({
|
||||
</i>
|
||||
</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 { TypeOptionController } from '$app/stores/effects/database/field/type_option/type_option_controller';
|
||||
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 { DatePickerPopup } from '$app/components/_shared/EditRow/DatePickerPopup';
|
||||
import { DragDropContext, Droppable, OnDragEndResponder } from 'react-beautiful-dnd';
|
||||
import { EditCellOptionPopup } from '$app/components/_shared/EditRow/EditCellOptionPopup';
|
||||
|
||||
export const EditRow = ({
|
||||
onClose,
|
||||
@ -34,11 +35,11 @@ export const EditRow = ({
|
||||
const [editingCell, setEditingCell] = useState<CellIdentifier | null>(null);
|
||||
const [showFieldEditor, setShowFieldEditor] = useState(false);
|
||||
const [editFieldTop, setEditFieldTop] = useState(0);
|
||||
const [editFieldRight, setEditFieldRight] = useState(0);
|
||||
const [editFieldLeft, setEditFieldLeft] = useState(0);
|
||||
|
||||
const [showChangeFieldTypePopup, setShowChangeFieldTypePopup] = useState(false);
|
||||
const [changeFieldTypeTop, setChangeFieldTypeTop] = useState(0);
|
||||
const [changeFieldTypeRight, setChangeFieldTypeRight] = useState(0);
|
||||
const [changeFieldTypeLeft, setChangeFieldTypeLeft] = useState(0);
|
||||
|
||||
const [showChangeOptionsPopup, setShowChangeOptionsPopup] = useState(false);
|
||||
const [changeOptionsTop, setChangeOptionsTop] = useState(0);
|
||||
@ -48,6 +49,12 @@ export const EditRow = ({
|
||||
const [datePickerTop, setDatePickerTop] = 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(() => {
|
||||
setUnveil(true);
|
||||
}, []);
|
||||
@ -59,10 +66,10 @@ export const EditRow = ({
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const onEditFieldClick = (cellIdentifier: CellIdentifier, top: number, right: number) => {
|
||||
const onEditFieldClick = (cellIdentifier: CellIdentifier, left: number, top: number) => {
|
||||
setEditingCell(cellIdentifier);
|
||||
setEditFieldTop(top);
|
||||
setEditFieldRight(right);
|
||||
setEditFieldLeft(left + 10);
|
||||
setShowFieldEditor(true);
|
||||
};
|
||||
|
||||
@ -74,7 +81,7 @@ export const EditRow = ({
|
||||
|
||||
const onChangeFieldTypeClick = (buttonTop: number, buttonRight: number) => {
|
||||
setChangeFieldTypeTop(buttonTop);
|
||||
setChangeFieldTypeRight(buttonRight);
|
||||
setChangeFieldTypeLeft(buttonRight + 30);
|
||||
setShowChangeFieldTypePopup(true);
|
||||
};
|
||||
|
||||
@ -95,17 +102,24 @@ export const EditRow = ({
|
||||
const onEditOptionsClick = async (cellIdentifier: CellIdentifier, left: number, top: number) => {
|
||||
setEditingCell(cellIdentifier);
|
||||
setChangeOptionsLeft(left);
|
||||
setChangeOptionsTop(top);
|
||||
setChangeOptionsTop(top + 40);
|
||||
setShowChangeOptionsPopup(true);
|
||||
};
|
||||
|
||||
const onEditDateClick = async (cellIdentifier: CellIdentifier, left: number, top: number) => {
|
||||
setEditingCell(cellIdentifier);
|
||||
setDatePickerLeft(left);
|
||||
setDatePickerTop(top);
|
||||
setDatePickerTop(top + 40);
|
||||
setShowDatePicker(true);
|
||||
};
|
||||
|
||||
const onOpenOptionDetailClick = (_left: number, _top: number, _select_option: SelectOptionPB) => {
|
||||
setEditingSelectOption(_select_option);
|
||||
setShowEditCellOption(true);
|
||||
setEditCellOptionLeft(_left);
|
||||
setEditCellOptionTop(_top);
|
||||
};
|
||||
|
||||
const onDragEnd: OnDragEndResponder = (result) => {
|
||||
if (!result.destination?.index) return;
|
||||
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 ${
|
||||
unveil ? 'opacity-100' : 'opacity-0'
|
||||
}`}
|
||||
onClick={() => onCloseClick()}
|
||||
>
|
||||
<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 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'}>
|
||||
<button className={'block h-8 w-8 rounded-lg text-shade-2 hover:bg-main-secondary'}>
|
||||
<CloseSvg></CloseSvg>
|
||||
@ -145,11 +165,9 @@ export const EditRow = ({
|
||||
cellIdentifier={cell.cellIdentifier}
|
||||
cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
|
||||
fieldController={controller.fieldController}
|
||||
onEditFieldClick={(top: number, right: number) => onEditFieldClick(cell.cellIdentifier, top, right)}
|
||||
onEditOptionsClick={(left: number, top: number) =>
|
||||
onEditOptionsClick(cell.cellIdentifier, left, top)
|
||||
}
|
||||
onEditDateClick={(left: number, top: number) => onEditDateClick(cell.cellIdentifier, left, top)}
|
||||
onEditFieldClick={onEditFieldClick}
|
||||
onEditOptionsClick={onEditOptionsClick}
|
||||
onEditDateClick={onEditDateClick}
|
||||
></EditCellWrapper>
|
||||
))}
|
||||
</div>
|
||||
@ -172,7 +190,7 @@ export const EditRow = ({
|
||||
{showFieldEditor && editingCell && (
|
||||
<EditFieldPopup
|
||||
top={editFieldTop}
|
||||
right={editFieldRight}
|
||||
left={editFieldLeft}
|
||||
cellIdentifier={editingCell}
|
||||
viewId={viewId}
|
||||
onOutsideClick={onOutsideEditFieldClick}
|
||||
@ -183,7 +201,7 @@ export const EditRow = ({
|
||||
{showChangeFieldTypePopup && (
|
||||
<ChangeFieldTypePopup
|
||||
top={changeFieldTypeTop}
|
||||
right={changeFieldTypeRight}
|
||||
left={changeFieldTypeLeft}
|
||||
onClick={(newType) => changeFieldType(newType)}
|
||||
onOutsideClick={() => setShowChangeFieldTypePopup(false)}
|
||||
></ChangeFieldTypePopup>
|
||||
@ -196,6 +214,7 @@ export const EditRow = ({
|
||||
cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
|
||||
fieldController={controller.fieldController}
|
||||
onOutsideClick={() => setShowChangeOptionsPopup(false)}
|
||||
openOptionDetail={onOpenOptionDetailClick}
|
||||
></CellOptionsPopup>
|
||||
)}
|
||||
{showDatePicker && editingCell && (
|
||||
@ -208,6 +227,17 @@ export const EditRow = ({
|
||||
onOutsideClick={() => setShowDatePicker(false)}
|
||||
></DatePickerPopup>
|
||||
)}
|
||||
{showEditCellOption && editingCell && editingSelectOption && (
|
||||
<EditCellOptionPopup
|
||||
top={editCellOptionTop}
|
||||
left={editCellOptionLeft}
|
||||
cellIdentifier={editingCell}
|
||||
editingSelectOption={editingSelectOption}
|
||||
onOutsideClick={() => {
|
||||
setShowEditCellOption(false);
|
||||
}}
|
||||
></EditCellOptionPopup>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { IPopupItem, Popup } from './Popup';
|
||||
import { IPopupItem, PopupSelect } from './PopupSelect';
|
||||
import i18n from 'i18next';
|
||||
|
||||
const supportedLanguages: { key: string; title: string }[] = [
|
||||
@ -37,11 +37,11 @@ export const LanguageSelectPopup = ({ onClose }: { onClose: () => void }) => {
|
||||
icon: <></>,
|
||||
}));
|
||||
return (
|
||||
<Popup
|
||||
<PopupSelect
|
||||
items={items}
|
||||
className={'absolute top-full right-0 z-10 w-[200px]'}
|
||||
onOutsideClick={onClose}
|
||||
columns={2}
|
||||
></Popup>
|
||||
></PopupSelect>
|
||||
);
|
||||
};
|
||||
|
@ -2,12 +2,12 @@ import { MouseEvent, ReactNode, useRef } from 'react';
|
||||
import useOutsideClick from './useOutsideClick';
|
||||
|
||||
export interface IPopupItem {
|
||||
icon: ReactNode;
|
||||
icon: ReactNode | (() => JSX.Element);
|
||||
title: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export const Popup = ({
|
||||
export const PopupSelect = ({
|
||||
items,
|
||||
className = '',
|
||||
onOutsideClick,
|
||||
@ -31,18 +31,20 @@ export const Popup = ({
|
||||
return (
|
||||
<div ref={ref} className={`${className} rounded-lg bg-white px-2 py-2 shadow-md`} style={style}>
|
||||
<div
|
||||
className={`grid ${columns === 1 && 'grid-cols-1'} ${columns === 2 && 'grid-cols-2'} ${
|
||||
columns === 3 && 'grid-cols-3'
|
||||
} gap-x-4`}
|
||||
className={
|
||||
(columns === 2 ? 'grid grid-cols-2' : '') + (columns === 3 ? 'grid grid-cols-3' : '') + ' w-full gap-x-4'
|
||||
}
|
||||
>
|
||||
{items.map((item, index) => (
|
||||
<button
|
||||
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)}
|
||||
>
|
||||
{item.icon}
|
||||
<>
|
||||
{typeof item.icon === 'function' ? item.icon() : item.icon}
|
||||
<span className={'flex-shrink-0'}>{item.title}</span>
|
||||
</>
|
||||
</button>
|
||||
))}
|
||||
</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);
|
||||
},
|
||||
});
|
||||
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) {
|
||||
const fieldId = await controller.getGroupByFieldId();
|
||||
|
@ -1,4 +1,4 @@
|
||||
export const EyeClosed = () => {
|
||||
export const EyeClosedSvg = () => {
|
||||
return (
|
||||
<svg width='100%' height='100%' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
|
@ -1,4 +1,4 @@
|
||||
export const EyeOpened = () => {
|
||||
export const EyeOpenSvg = () => {
|
||||
return (
|
||||
<svg width='100%' height='100%' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<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 { EyeClosed } from '../../_shared/svg/EyeClosedSvg';
|
||||
import { EyeOpened } from '../../_shared/svg/EyeOpenSvg';
|
||||
import { EyeClosedSvg } from '../../_shared/svg/EyeClosedSvg';
|
||||
import { EyeOpenSvg } from '../../_shared/svg/EyeOpenSvg';
|
||||
import { useLogin } from './Login.hooks';
|
||||
import { Link } from 'react-router-dom';
|
||||
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 '
|
||||
onClick={onTogglePassword}
|
||||
>
|
||||
<span className='h-6 w-6'>{showPassword ? <EyeClosed /> : <EyeOpened />}</span>
|
||||
<span className='h-6 w-6'>{showPassword ? <EyeClosedSvg /> : <EyeOpenSvg />}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { AppflowyLogo } from '../../_shared/svg/AppflowyLogo';
|
||||
import { EyeClosed } from '../../_shared/svg/EyeClosedSvg';
|
||||
import { EyeOpened } from '../../_shared/svg/EyeOpenSvg';
|
||||
import { EyeClosedSvg } from '../../_shared/svg/EyeClosedSvg';
|
||||
import { EyeOpenSvg } from '../../_shared/svg/EyeOpenSvg';
|
||||
|
||||
import { useSignUp } from './SignUp.hooks';
|
||||
import { Link } from 'react-router-dom';
|
||||
@ -71,7 +71,7 @@ export const SignUp = () => {
|
||||
onClick={onTogglePassword}
|
||||
type='button'
|
||||
>
|
||||
<span className='h-6 w-6'>{showPassword ? <EyeClosed /> : <EyeOpened />}</span>
|
||||
<span className='h-6 w-6'>{showPassword ? <EyeClosedSvg /> : <EyeOpenSvg />}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -89,7 +89,7 @@ export const SignUp = () => {
|
||||
onClick={onToggleConfirmPassword}
|
||||
type='button'
|
||||
>
|
||||
<span className='h-6 w-6'>{showConfirmPassword ? <EyeClosed /> : <EyeOpened />}</span>
|
||||
<span className='h-6 w-6'>{showConfirmPassword ? <EyeClosedSvg /> : <EyeOpenSvg />}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { SettingsSvg } from '../_shared/svg/SettingsSvg';
|
||||
import { SearchInput } from '../_shared/SearchInput';
|
||||
import { BoardBlock } from './BoardBlock';
|
||||
import { BoardGroup } from './BoardGroup';
|
||||
import { NewBoardBlock } from './NewBoardBlock';
|
||||
import { useDatabase } from '../_shared/database-hooks/useDatabase';
|
||||
import { ViewLayoutPB } from '@/services/backend';
|
||||
@ -8,8 +7,9 @@ import { DragDropContext } from 'react-beautiful-dnd';
|
||||
import { useState } from 'react';
|
||||
import { RowInfo } from '$app/stores/effects/database/row/row_cache';
|
||||
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 [showBoardRow, setShowBoardRow] = useState(false);
|
||||
const [boardRowInfo, setBoardRowInfo] = useState<RowInfo>();
|
||||
@ -22,12 +22,7 @@ export const Board = ({ viewId }: { viewId: string }) => {
|
||||
return (
|
||||
<>
|
||||
<div className='flex w-full items-center justify-between'>
|
||||
<div className={'flex items-center text-xl font-semibold'}>
|
||||
<div>{'Kanban'}</div>
|
||||
<button className={'ml-2 h-5 w-5'}>
|
||||
<SettingsSvg></SettingsSvg>
|
||||
</button>
|
||||
</div>
|
||||
<BoardToolbar title={title} />
|
||||
|
||||
<div className='flex shrink-0 items-center gap-4'>
|
||||
<SearchInput />
|
||||
@ -39,7 +34,7 @@ export const Board = ({ viewId }: { viewId: string }) => {
|
||||
{controller &&
|
||||
groups &&
|
||||
groups.map((group, index) => (
|
||||
<BoardBlock
|
||||
<BoardGroup
|
||||
key={group.groupId}
|
||||
viewId={viewId}
|
||||
controller={controller}
|
||||
|
@ -1,9 +1,10 @@
|
||||
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 { DatabaseController } from '../../stores/effects/database/database_controller';
|
||||
import { DatabaseController } from '$app/stores/effects/database/database_controller';
|
||||
import { BoardCell } from './BoardCell';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
import { MouseEventHandler } from 'react';
|
||||
|
||||
export const BoardCard = ({
|
||||
index,
|
||||
@ -22,6 +23,11 @@ export const BoardCard = ({
|
||||
}) => {
|
||||
const { cells } = useRow(viewId, controller, rowInfo);
|
||||
|
||||
const onDetailClick: MouseEventHandler = (e) => {
|
||||
e.stopPropagation();
|
||||
// onOpenRow(rowInfo);
|
||||
};
|
||||
|
||||
return (
|
||||
<Draggable draggableId={rowInfo.row.id} index={index}>
|
||||
{(provided) => (
|
||||
@ -32,7 +38,7 @@ export const BoardCard = ({
|
||||
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 `}
|
||||
>
|
||||
<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>
|
||||
</button>
|
||||
<div className={'flex flex-col gap-3'}>
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { CellIdentifier } from '../../stores/effects/database/cell/cell_bd_svc';
|
||||
import { CellCache } from '../../stores/effects/database/cell/cell_cache';
|
||||
import { FieldController } from '../../stores/effects/database/field/field_controller';
|
||||
import { FieldType } from '../../../services/backend';
|
||||
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 { FieldType } from '@/services/backend';
|
||||
import { BoardOptionsCell } from './BoardOptionsCell';
|
||||
import { BoardDateCell } from './BoardDateCell';
|
||||
import { BoardTextCell } from './BoardTextCell';
|
||||
import { BoardUrlCell } from '$app/components/board/BoardUrlCell';
|
||||
import { BoardCheckboxCell } from '$app/components/board/BoardCheckboxCell';
|
||||
|
||||
export const BoardCell = ({
|
||||
cellIdentifier,
|
||||
@ -38,6 +39,12 @@ export const BoardCell = ({
|
||||
cellCache={cellCache}
|
||||
fieldController={fieldController}
|
||||
></BoardUrlCell>
|
||||
) : cellIdentifier.fieldType === FieldType.Checkbox ? (
|
||||
<BoardCheckboxCell
|
||||
cellIdentifier={cellIdentifier}
|
||||
cellCache={cellCache}
|
||||
fieldController={fieldController}
|
||||
></BoardCheckboxCell>
|
||||
) : (
|
||||
<BoardTextCell
|
||||
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 { CellIdentifier } from '../../stores/effects/database/cell/cell_bd_svc';
|
||||
import { CellCache } from '../../stores/effects/database/cell/cell_cache';
|
||||
import { FieldController } from '../../stores/effects/database/field/field_controller';
|
||||
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';
|
||||
|
||||
export const BoardDateCell = ({
|
||||
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 AddSvg from '../_shared/svg/AddSvg';
|
||||
import { BoardCard } from './BoardCard';
|
||||
import { RowInfo } from '../../stores/effects/database/row/row_cache';
|
||||
import { DatabaseController } from '../../stores/effects/database/database_controller';
|
||||
import { RowInfo } from '$app/stores/effects/database/row/row_cache';
|
||||
import { DatabaseController } from '$app/stores/effects/database/database_controller';
|
||||
import { Droppable } from 'react-beautiful-dnd';
|
||||
import { DatabaseGroupController } from '$app/stores/effects/database/group/group_controller';
|
||||
|
||||
export const BoardBlock = ({
|
||||
export const BoardGroup = ({
|
||||
viewId,
|
||||
controller,
|
||||
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 { CellIdentifier } from '../../stores/effects/database/cell/cell_bd_svc';
|
||||
import { CellCache } from '../../stores/effects/database/cell/cell_cache';
|
||||
import { FieldController } from '../../stores/effects/database/field/field_controller';
|
||||
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 { getBgColor } from '$app/components/_shared/getColor';
|
||||
|
||||
export const BoardOptionsCell = ({
|
||||
@ -18,7 +18,7 @@ export const BoardOptionsCell = ({
|
||||
|
||||
return (
|
||||
<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}>
|
||||
{option?.name || ''}
|
||||
</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 (
|
||||
<>
|
||||
<a
|
||||
className={'text-main-accent hover:underline'}
|
||||
href={(data as URLCellDataPB | undefined)?.url || ''}
|
||||
target={'_blank'}
|
||||
>
|
||||
{(data as URLCellDataPB | undefined)?.content || ''}
|
||||
<a className={'text-main-accent hover:underline'} href={(data as URLCellDataPB)?.url || ''} target={'_blank'}>
|
||||
{(data as URLCellDataPB)?.content || ''}
|
||||
</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 { debounce } from '@/appflowy_app/utils/tool';
|
||||
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.top = '1px';
|
||||
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';
|
||||
} else if (node.data.style?.level === 2) {
|
||||
} else if (nodeData.level === 2) {
|
||||
el.style.top = '6px';
|
||||
} else {
|
||||
el.style.top = '5px';
|
||||
@ -80,16 +81,7 @@ function useController() {
|
||||
const parentId = node.parent;
|
||||
if (!parentId || !controller) return;
|
||||
|
||||
controller.transact([
|
||||
() => {
|
||||
const newNode = {
|
||||
id: v4(),
|
||||
delta: [],
|
||||
type: BlockType.TextBlock,
|
||||
};
|
||||
controller.insert(newNode, parentId, node.id);
|
||||
},
|
||||
]);
|
||||
//
|
||||
}, []);
|
||||
|
||||
return {
|
||||
|
@ -3,11 +3,18 @@ import { useDocumentTitle } from './DocumentTitle.hooks';
|
||||
import TextBlock from '../TextBlock';
|
||||
|
||||
export default function DocumentTitle({ id }: { id: string }) {
|
||||
const { node, delta } = useDocumentTitle(id);
|
||||
const { node } = useDocumentTitle(id);
|
||||
if (!node) return null;
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ const fontSize: Record<string, string> = {
|
||||
export default function HeadingBlock({ node, delta }: { node: Node; delta: TextDelta[] }) {
|
||||
return (
|
||||
<div className={`${fontSize[node.data.style?.level]} font-semibold `}>
|
||||
<TextBlock node={node} childIds={[]} delta={delta} />
|
||||
{/*<TextBlock node={node} childIds={[]} delta={delta} />*/}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ export default function ListBlock({ node, delta }: { node: Node; delta: TextDelt
|
||||
if (node.data.style?.type === 'column') return <></>;
|
||||
return (
|
||||
<div className='flex-1'>
|
||||
<TextBlock delta={delta} node={node} childIds={[]} />
|
||||
{/*<TextBlock delta={delta} node={node} childIds={[]} />*/}
|
||||
</div>
|
||||
);
|
||||
}, [node, delta]);
|
||||
|
@ -6,7 +6,7 @@ const defaultSize = 60;
|
||||
export function useVirtualizedList(count: number) {
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const Virtualize = useVirtualizer({
|
||||
const virtualize = useVirtualizer({
|
||||
count,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => {
|
||||
@ -15,7 +15,7 @@ export function useVirtualizedList(count: number) {
|
||||
});
|
||||
|
||||
return {
|
||||
Virtualize: Virtualize,
|
||||
virtualize,
|
||||
parentRef,
|
||||
};
|
||||
}
|
||||
|
@ -13,8 +13,8 @@ export default function VirtualizedList({
|
||||
node: Node;
|
||||
renderNode: (nodeId: string) => JSX.Element;
|
||||
}) {
|
||||
const { Virtualize, parentRef } = useVirtualizedList(childIds.length);
|
||||
const virtualItems = Virtualize.getVirtualItems();
|
||||
const { virtualize, parentRef } = useVirtualizedList(childIds.length);
|
||||
const virtualItems = virtualize.getVirtualItems();
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -25,7 +25,7 @@ export default function VirtualizedList({
|
||||
<div
|
||||
className='doc-body max-w-screen w-[900px] min-w-0'
|
||||
style={{
|
||||
height: Virtualize.getTotalSize(),
|
||||
height: virtualize.getTotalSize(),
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
@ -42,7 +42,7 @@ export default function VirtualizedList({
|
||||
{virtualItems.map((virtualRow) => {
|
||||
const id = childIds[virtualRow.index];
|
||||
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}
|
||||
{renderNode(id)}
|
||||
</div>
|
||||
|
@ -2,7 +2,7 @@ import { useAppDispatch, useAppSelector } from '../../stores/store';
|
||||
import { errorActions } from '../../stores/reducers/error/slice';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export const useError = () => {
|
||||
export const useError = (e: Error) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const error = useAppSelector((state) => state.error);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
@ -13,6 +13,12 @@ export const useError = () => {
|
||||
setErrorMessage(error.message);
|
||||
}, [error]);
|
||||
|
||||
useEffect(() => {
|
||||
if (e) {
|
||||
showError(e.message);
|
||||
}
|
||||
}, [e]);
|
||||
|
||||
const showError = (msg: string) => {
|
||||
dispatch(errorActions.showError(msg));
|
||||
};
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { useError } from './Error.hooks';
|
||||
import { ErrorModal } from './ErrorModal';
|
||||
|
||||
export const ErrorHandlerPage = () => {
|
||||
const { hideError, errorMessage, displayError } = useError();
|
||||
export const ErrorHandlerPage = ({ error }: { error: Error }) => {
|
||||
const { hideError, errorMessage, displayError } = useError(error);
|
||||
|
||||
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';
|
||||
|
||||
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 = () => {
|
||||
const { grid } = useAppSelector((state) => state);
|
||||
|
@ -1,27 +1,25 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
import { FieldType } from '@/services/backend/models/flowy-database/field_entities';
|
||||
import { gridActions } from '../../../stores/reducers/grid/slice';
|
||||
import { useAppDispatch, useAppSelector } from '../../../stores/store';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
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 { None } from 'ts-results';
|
||||
|
||||
export const useGridTableHeaderHooks = function () {
|
||||
const dispatch = useAppDispatch();
|
||||
const grid = useAppSelector((state) => state.grid);
|
||||
export const useGridTableHeaderHooks = function (controller: DatabaseController) {
|
||||
const database = useAppSelector((state) => state.database);
|
||||
|
||||
const onAddField = () => {
|
||||
dispatch(
|
||||
gridActions.addField({
|
||||
field: {
|
||||
fieldId: nanoid(8),
|
||||
name: 'Name',
|
||||
fieldOptions: {},
|
||||
fieldType: FieldType.RichText,
|
||||
},
|
||||
})
|
||||
);
|
||||
const onAddField = async () => {
|
||||
// TODO: move this to database controller hook
|
||||
const fieldController = new TypeOptionController(controller.viewId, None);
|
||||
await fieldController.initialize();
|
||||
};
|
||||
|
||||
return {
|
||||
fields: grid.fields,
|
||||
fields: Object.values(database.fields).map((field) => {
|
||||
return {
|
||||
fieldId: field.fieldId,
|
||||
name: field.title,
|
||||
fieldType: field.fieldType,
|
||||
};
|
||||
}),
|
||||
onAddField,
|
||||
};
|
||||
};
|
||||
|
@ -1,49 +1,32 @@
|
||||
import { DatabaseController } from '@/appflowy_app/stores/effects/database/database_controller';
|
||||
|
||||
import AddSvg from '../../_shared/svg/AddSvg';
|
||||
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 = () => {
|
||||
const { fields, onAddField } = useGridTableHeaderHooks();
|
||||
import { GridTableHeaderItem } from './GridTableHeaderItem';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const GridTableHeader = ({ controller }: { controller: DatabaseController }) => {
|
||||
const { fields, onAddField } = useGridTableHeaderHooks(controller);
|
||||
const { t } = useTranslation('');
|
||||
|
||||
return (
|
||||
<>
|
||||
<thead>
|
||||
<tr>
|
||||
{fields.map((field, i) => {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
return <GridTableHeaderItem field={field} controller={controller} key={i} />;
|
||||
})}
|
||||
|
||||
<th className='m-0 w-40 border border-r-0 border-shade-6 p-0'>
|
||||
<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}
|
||||
>
|
||||
<i className='mr-2 h-5 w-5'>
|
||||
<AddSvg />
|
||||
</i>
|
||||
<span>New column</span>
|
||||
<span>{t('grid.field.newColumn')}</span>
|
||||
</div>
|
||||
</th>
|
||||
</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 { useAppDispatch } from '../../../stores/store';
|
||||
import { DatabaseController } from '@/appflowy_app/stores/effects/database/database_controller';
|
||||
|
||||
export const useGridAddRow = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
function addRow() {
|
||||
dispatch(gridActions.addRow());
|
||||
export const useGridAddRow = (controller: DatabaseController) => {
|
||||
async function addRow() {
|
||||
await controller.createRow();
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -1,7 +1,10 @@
|
||||
import { DatabaseController } from '@/appflowy_app/stores/effects/database/database_controller';
|
||||
import AddSvg from '../../_shared/svg/AddSvg';
|
||||
import { useGridAddRow } from './GridAddRow.hooks';
|
||||
export const GridAddRow = () => {
|
||||
const { addRow } = useGridAddRow();
|
||||
import { useTranslation } from 'react-i18next';
|
||||
export const GridAddRow = ({ controller }: { controller: DatabaseController }) => {
|
||||
const { addRow } = useGridAddRow(controller);
|
||||
const { t } = useTranslation('');
|
||||
|
||||
return (
|
||||
<div>
|
||||
@ -9,7 +12,7 @@ export const GridAddRow = () => {
|
||||
<i className='mr-2 h-5 w-5'>
|
||||
<AddSvg />
|
||||
</i>
|
||||
<span>New row</span>
|
||||
<span>{t('grid.row.newRow')}</span>
|
||||
</button>
|
||||
</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 { useGridTableRowsHooks } from './GridTableRows.hooks';
|
||||
|
||||
export const GridTableRows = () => {
|
||||
const { rows } = useGridTableRowsHooks();
|
||||
import { DatabaseController } from '@/appflowy_app/stores/effects/database/database_controller';
|
||||
import { RowInfo } from '@/appflowy_app/stores/effects/database/row/row_cache';
|
||||
import { GridTableRow } from './GridTableRow';
|
||||
export const GridTableRows = ({
|
||||
viewId,
|
||||
controller,
|
||||
allRows,
|
||||
onOpenRow,
|
||||
}: {
|
||||
viewId: string;
|
||||
controller: DatabaseController;
|
||||
allRows: readonly RowInfo[];
|
||||
onOpenRow: (rowId: RowInfo) => void;
|
||||
}) => {
|
||||
return (
|
||||
<tbody>
|
||||
{rows.map((row, i) => {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
{allRows.map((row, i) => {
|
||||
return <GridTableRow onOpenRow={onOpenRow} row={row} key={i} viewId={viewId} controller={controller} />;
|
||||
})}
|
||||
</tbody>
|
||||
);
|
||||
|
@ -1,7 +1,5 @@
|
||||
import { useAppDispatch, useAppSelector } from '@/appflowy_app/stores/store';
|
||||
import { useState } from 'react';
|
||||
import { gridActions } from '../../../stores/reducers/grid/slice';
|
||||
|
||||
import { useAppDispatch, useAppSelector } from '../../../stores/store';
|
||||
|
||||
export const useGridTitleHooks = function () {
|
||||
const dispatch = useAppDispatch();
|
||||
@ -9,16 +7,12 @@ export const useGridTitleHooks = function () {
|
||||
|
||||
const [title, setTitle] = useState(grid.title);
|
||||
const [changingTitle, setChangingTitle] = useState(false);
|
||||
const [showOptions, setShowOptions] = useState(false);
|
||||
|
||||
const onTitleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setTitle(event.target.value);
|
||||
};
|
||||
|
||||
const onTitleBlur = () => {
|
||||
dispatch(gridActions.updateGridTitle({ title }));
|
||||
setChangingTitle(false);
|
||||
};
|
||||
|
||||
const onTitleClick = () => {
|
||||
setChangingTitle(true);
|
||||
};
|
||||
@ -26,8 +20,9 @@ export const useGridTitleHooks = function () {
|
||||
return {
|
||||
title,
|
||||
onTitleChange,
|
||||
onTitleBlur,
|
||||
onTitleClick,
|
||||
changingTitle,
|
||||
showOptions,
|
||||
setShowOptions,
|
||||
};
|
||||
};
|
||||
|
@ -1,15 +1,21 @@
|
||||
import { useGridTitleHooks } from './GridTitle.hooks';
|
||||
import { SettingsSvg } from '../../_shared/svg/SettingsSvg';
|
||||
import { GridTitleOptionsPopup } from './GridTitleOptionsPopup';
|
||||
|
||||
export const GridTitle = () => {
|
||||
const { title } = useGridTitleHooks();
|
||||
const { title, showOptions, setShowOptions } = useGridTitleHooks();
|
||||
|
||||
return (
|
||||
<div className={'flex items-center text-xl font-semibold'}>
|
||||
<div className={'relative flex items-center '}>
|
||||
<div>{title}</div>
|
||||
<button className={'ml-2 h-5 w-5'}>
|
||||
|
||||
<div className='flex '>
|
||||
<button className={'ml-2 h-5 w-5 '} onClick={() => setShowOptions(!showOptions)}>
|
||||
<SettingsSvg></SettingsSvg>
|
||||
</button>
|
||||
|
||||
{showOptions && <GridTitleOptionsPopup onClose={() => setShowOptions(!showOptions)} />}
|
||||
</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 { SearchInput } from '../../_shared/SearchInput';
|
||||
import { GridSortButton } from './GridSortButton';
|
||||
import { GridFieldsButton } from './GridFieldsButton';
|
||||
import { GridFilterButton } from './GridFilterButton';
|
||||
|
||||
export const GridToolbar = () => {
|
||||
return (
|
||||
<div className='flex shrink-0 items-center gap-4'>
|
||||
<SearchInput />
|
||||
<GridAddView />
|
||||
<GridFilterButton></GridFilterButton>
|
||||
<GridSortButton></GridSortButton>
|
||||
<GridFieldsButton></GridFieldsButton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { IPopupItem, Popup } from '../../_shared/Popup';
|
||||
import { IPopupItem, PopupSelect } from '../../_shared/PopupSelect';
|
||||
import { LogoutSvg } from '../../_shared/svg/LogoutSvg';
|
||||
|
||||
export const OptionsPopup = ({ onSignOutClick, onClose }: { onSignOutClick: () => void; onClose: () => void }) => {
|
||||
@ -14,10 +14,10 @@ export const OptionsPopup = ({ onSignOutClick, onClose }: { onSignOutClick: () =
|
||||
},
|
||||
];
|
||||
return (
|
||||
<Popup
|
||||
<PopupSelect
|
||||
className={'absolute top-[50px] right-[30px] z-10 whitespace-nowrap'}
|
||||
items={items}
|
||||
onOutsideClick={onClose}
|
||||
></Popup>
|
||||
></PopupSelect>
|
||||
);
|
||||
};
|
||||
|
@ -5,7 +5,7 @@ import { IPage, pagesActions } from '../../../stores/reducers/pages/slice';
|
||||
import { ViewLayoutPB } from '@/services/backend';
|
||||
import { AppBackendService } from '../../../stores/effects/folder/app/app_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 { useNavigate } from 'react-router-dom';
|
||||
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 workspaceBackendService = new WorkspaceBackendService(workspace.id || '');
|
||||
|
||||
// Error
|
||||
const error = useError();
|
||||
|
||||
useEffect(() => {
|
||||
void appObserver.subscribe({
|
||||
onAppChanged: (change) => {
|
||||
@ -85,12 +82,8 @@ export const useFolderEvents = (folder: IFolder, pages: IPage[]) => {
|
||||
};
|
||||
|
||||
const changeFolderTitle = async (newTitle: string) => {
|
||||
try {
|
||||
await appBackendService.update({ name: newTitle });
|
||||
appDispatch(foldersActions.renameFolder({ id: folder.id, newTitle }));
|
||||
} catch (e: any) {
|
||||
error.showError(e?.message);
|
||||
}
|
||||
};
|
||||
|
||||
const closeRenamePopup = () => {
|
||||
@ -99,24 +92,16 @@ export const useFolderEvents = (folder: IFolder, pages: IPage[]) => {
|
||||
|
||||
const deleteFolder = async () => {
|
||||
closePopup();
|
||||
try {
|
||||
await appBackendService.delete();
|
||||
appDispatch(foldersActions.deleteFolder({ id: folder.id }));
|
||||
} catch (e: any) {
|
||||
error.showError(e?.message);
|
||||
}
|
||||
};
|
||||
|
||||
const duplicateFolder = async () => {
|
||||
closePopup();
|
||||
try {
|
||||
const newApp = await workspaceBackendService.createApp({
|
||||
name: folder.title,
|
||||
});
|
||||
appDispatch(foldersActions.addFolder({ id: newApp.id, title: folder.title }));
|
||||
} catch (e: any) {
|
||||
error.showError(e?.message);
|
||||
}
|
||||
};
|
||||
|
||||
const closePopup = () => {
|
||||
@ -126,7 +111,6 @@ export const useFolderEvents = (folder: IFolder, pages: IPage[]) => {
|
||||
|
||||
const onAddNewDocumentPage = async () => {
|
||||
closePopup();
|
||||
try {
|
||||
const newView = await appBackendService.createView({
|
||||
name: 'New Document 1',
|
||||
layoutType: ViewLayoutPB.Document,
|
||||
@ -144,14 +128,10 @@ export const useFolderEvents = (folder: IFolder, pages: IPage[]) => {
|
||||
setShowPages(true);
|
||||
|
||||
navigate(`/page/document/${newView.id}`);
|
||||
} catch (e: any) {
|
||||
error.showError(e?.message);
|
||||
}
|
||||
};
|
||||
|
||||
const onAddNewBoardPage = async () => {
|
||||
closePopup();
|
||||
try {
|
||||
const newView = await appBackendService.createView({
|
||||
name: 'New Board 1',
|
||||
layoutType: ViewLayoutPB.Board,
|
||||
@ -169,14 +149,10 @@ export const useFolderEvents = (folder: IFolder, pages: IPage[]) => {
|
||||
);
|
||||
|
||||
navigate(`/page/board/${newView.id}`);
|
||||
} catch (e: any) {
|
||||
error.showError(e?.message);
|
||||
}
|
||||
};
|
||||
|
||||
const onAddNewGridPage = async () => {
|
||||
closePopup();
|
||||
try {
|
||||
const newView = await appBackendService.createView({
|
||||
name: 'New Grid 1',
|
||||
layoutType: ViewLayoutPB.Grid,
|
||||
@ -194,9 +170,6 @@ export const useFolderEvents = (folder: IFolder, pages: IPage[]) => {
|
||||
);
|
||||
|
||||
navigate(`/page/grid/${newView.id}`);
|
||||
} catch (e: any) {
|
||||
error.showError(e?.message);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { IPopupItem, Popup } from '../../_shared/Popup';
|
||||
import { IPopupItem, PopupSelect } from '../../_shared/PopupSelect';
|
||||
import { EditSvg } from '../../_shared/svg/EditSvg';
|
||||
import { TrashSvg } from '../../_shared/svg/TrashSvg';
|
||||
import { CopySvg } from '../../_shared/svg/CopySvg';
|
||||
@ -47,11 +47,11 @@ export const NavItemOptionsPopup = ({
|
||||
];
|
||||
|
||||
return (
|
||||
<Popup
|
||||
<PopupSelect
|
||||
onOutsideClick={() => onClose && onClose()}
|
||||
items={items}
|
||||
className={`absolute right-0`}
|
||||
style={{ top: `${top}px` }}
|
||||
></Popup>
|
||||
></PopupSelect>
|
||||
);
|
||||
};
|
||||
|
@ -117,6 +117,7 @@ export const NavigationPanel = ({
|
||||
{/*<PluginsButton></PluginsButton>*/}
|
||||
|
||||
<DesignSpec></DesignSpec>
|
||||
<AllIcons></AllIcons>
|
||||
<TestBackendButton></TestBackendButton>
|
||||
|
||||
{/*Trash Button*/}
|
||||
@ -158,7 +159,7 @@ export const TestBackendButton = () => {
|
||||
onClick={() => navigate('/page/api-test')}
|
||||
className={'flex w-full items-center rounded-lg px-4 py-2 hover:bg-surface-2'}
|
||||
>
|
||||
APITest
|
||||
API Test
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@ -171,7 +172,19 @@ export const DesignSpec = () => {
|
||||
onClick={() => navigate('page/colors')}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
@ -1,23 +1,17 @@
|
||||
import { useAppDispatch, useAppSelector } from '../../../stores/store';
|
||||
import { foldersActions } from '../../../stores/reducers/folders/slice';
|
||||
import { WorkspaceBackendService } from '../../../stores/effects/folder/workspace/workspace_bd_svc';
|
||||
import { useError } from '../../error/Error.hooks';
|
||||
|
||||
export const useNewFolder = () => {
|
||||
const appDispatch = useAppDispatch();
|
||||
const workspace = useAppSelector((state) => state.workspace);
|
||||
const workspaceBackendService = new WorkspaceBackendService(workspace.id || '');
|
||||
const error = useError();
|
||||
|
||||
const onNewFolder = async () => {
|
||||
try {
|
||||
const newApp = await workspaceBackendService.createApp({
|
||||
name: 'New Folder 1',
|
||||
});
|
||||
appDispatch(foldersActions.addFolder({ id: newApp.id, title: newApp.name }));
|
||||
} catch (e: any) {
|
||||
error.showError(e?.message);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { IPopupItem, Popup } from '../../_shared/Popup';
|
||||
import { IPopupItem, PopupSelect } from '../../_shared/PopupSelect';
|
||||
import { DocumentSvg } from '../../_shared/svg/DocumentSvg';
|
||||
import { BoardSvg } from '../../_shared/svg/BoardSvg';
|
||||
import { GridSvg } from '../../_shared/svg/GridSvg';
|
||||
@ -47,11 +47,11 @@ export const NewPagePopup = ({
|
||||
];
|
||||
|
||||
return (
|
||||
<Popup
|
||||
<PopupSelect
|
||||
onOutsideClick={() => onClose && onClose()}
|
||||
items={items}
|
||||
className={'absolute right-0'}
|
||||
style={{ top: `${top}px` }}
|
||||
></Popup>
|
||||
></PopupSelect>
|
||||
);
|
||||
};
|
||||
|
@ -13,7 +13,6 @@ export const usePageEvents = (page: IPage) => {
|
||||
const [activePageId, setActivePageId] = useState<string>('');
|
||||
const currentLocation = useLocation();
|
||||
const viewBackendService: ViewBackendService = new ViewBackendService(page.id);
|
||||
const error = useError();
|
||||
|
||||
useEffect(() => {
|
||||
const { pathname } = currentLocation;
|
||||
@ -32,33 +31,21 @@ export const usePageEvents = (page: IPage) => {
|
||||
};
|
||||
|
||||
const changePageTitle = async (newTitle: string) => {
|
||||
try {
|
||||
await viewBackendService.update({ name: newTitle });
|
||||
appDispatch(pagesActions.renamePage({ id: page.id, newTitle }));
|
||||
} catch (e: any) {
|
||||
error.showError(e?.message);
|
||||
}
|
||||
};
|
||||
|
||||
const deletePage = async () => {
|
||||
closePopup();
|
||||
try {
|
||||
await viewBackendService.delete();
|
||||
appDispatch(pagesActions.deletePage({ id: page.id }));
|
||||
} catch (e: any) {
|
||||
error.showError(e?.message);
|
||||
}
|
||||
};
|
||||
|
||||
const duplicatePage = () => {
|
||||
closePopup();
|
||||
try {
|
||||
appDispatch(
|
||||
pagesActions.addPage({ id: nanoid(8), pageType: page.pageType, title: page.title, folderId: page.folderId })
|
||||
);
|
||||
} catch (e: any) {
|
||||
error.showError(e?.message);
|
||||
}
|
||||
};
|
||||
|
||||
const closePopup = () => {
|
||||
|
@ -3,13 +3,12 @@ import { useAppDispatch, useAppSelector } from '../../stores/store';
|
||||
import { pagesActions } from '../../stores/reducers/pages/slice';
|
||||
import { workspaceActions } from '../../stores/reducers/workspace/slice';
|
||||
import { UserBackendService } from '../../stores/effects/user/user_bd_svc';
|
||||
import { useError } from '../error/Error.hooks';
|
||||
|
||||
export const useWorkspace = () => {
|
||||
const currentUser = useAppSelector((state) => state.currentUser);
|
||||
|
||||
const appDispatch = useAppDispatch();
|
||||
const error = useError();
|
||||
|
||||
const userBackendService: UserBackendService = new UserBackendService(currentUser.id || 0);
|
||||
|
||||
const loadWorkspaceItems = async () => {
|
||||
@ -31,15 +30,11 @@ export const useWorkspace = () => {
|
||||
}
|
||||
} catch (e1) {
|
||||
// create workspace for first start
|
||||
try {
|
||||
const workspace = await userBackendService.createWorkspace({ name: 'New Workspace', desc: '' });
|
||||
appDispatch(workspaceActions.updateWorkspace({ id: workspace.id, name: workspace.name }));
|
||||
|
||||
appDispatch(foldersActions.clearFolders());
|
||||
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());
|
||||
|
||||
// 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:
|
||||
// {
|
||||
// "document": {
|
||||
|
@ -12,14 +12,21 @@ export enum BlockType {
|
||||
TableBlock = 'table',
|
||||
ColumnBlock = 'column',
|
||||
}
|
||||
|
||||
export interface HeadingBlockData {
|
||||
level: number;
|
||||
}
|
||||
|
||||
export interface TextBlockData {
|
||||
delta: TextDelta[];
|
||||
}
|
||||
|
||||
export interface PageBlockData extends TextBlockData {}
|
||||
|
||||
export interface NestedBlock {
|
||||
id: string;
|
||||
type: BlockType;
|
||||
data: {
|
||||
delta?: TextDelta[];
|
||||
};
|
||||
externalId: string;
|
||||
externalType: 'text' | 'array' | 'map';
|
||||
data: Record<string, any>;
|
||||
parent: string | null;
|
||||
children: string;
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ export class DatabaseController {
|
||||
this.databaseViewCache.initializeWithRows(database.rows);
|
||||
|
||||
this._callback?.onViewChanged?.(database);
|
||||
return loadGroupResult;
|
||||
return Ok(database.rows);
|
||||
} else {
|
||||
return Err(openDatabaseResult.val);
|
||||
}
|
||||
|
@ -4,27 +4,45 @@ import {
|
||||
EditPayloadPB,
|
||||
FlowyError,
|
||||
OpenDocumentPayloadPB,
|
||||
DocumentDataPB2,
|
||||
ViewIdPB,
|
||||
OpenDocumentPayloadPBV2,
|
||||
ApplyActionPayloadPBV2,
|
||||
BlockActionTypePB,
|
||||
BlockActionPB,
|
||||
CloseDocumentPayloadPBV2,
|
||||
} from '@/services/backend';
|
||||
import { DocumentEventApplyEdit, DocumentEventGetDocument } from '@/services/backend/events/flowy-document';
|
||||
import { Result } from 'ts-results';
|
||||
import { FolderEventCloseView } from '@/services/backend/events/flowy-folder2';
|
||||
import {
|
||||
DocumentEvent2ApplyAction,
|
||||
DocumentEvent2CloseDocument,
|
||||
DocumentEvent2OpenDocument,
|
||||
} from '@/services/backend/events/flowy-document2';
|
||||
|
||||
export class DocumentBackendService {
|
||||
constructor(public readonly viewId: string) {}
|
||||
|
||||
open = (): Promise<Result<DocumentDataPB, FlowyError>> => {
|
||||
const payload = OpenDocumentPayloadPB.fromObject({ document_id: this.viewId, version: DocumentVersionPB.V1 });
|
||||
return DocumentEventGetDocument(payload);
|
||||
open = (): Promise<Result<DocumentDataPB2, FlowyError>> => {
|
||||
const payload = OpenDocumentPayloadPBV2.fromObject({
|
||||
document_id: this.viewId,
|
||||
});
|
||||
return DocumentEvent2OpenDocument(payload);
|
||||
};
|
||||
|
||||
applyEdit = (operations: string) => {
|
||||
const payload = EditPayloadPB.fromObject({ doc_id: this.viewId, operations: operations });
|
||||
return DocumentEventApplyEdit(payload);
|
||||
applyActions = (actions: [BlockActionPB]): Promise<Result<void, FlowyError>> => {
|
||||
const payload = ApplyActionPayloadPBV2.fromObject({
|
||||
document_id: this.viewId,
|
||||
actions: actions,
|
||||
});
|
||||
return DocumentEvent2ApplyAction(payload);
|
||||
};
|
||||
|
||||
close = () => {
|
||||
const payload = ViewIdPB.fromObject({ value: this.viewId });
|
||||
return FolderEventCloseView(payload);
|
||||
close = (): Promise<Result<void, FlowyError>> => {
|
||||
const payload = CloseDocumentPayloadPBV2.fromObject({
|
||||
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 { DocumentBackendService } from './document_bd_svc';
|
||||
import { FlowyError } from '@/services/backend';
|
||||
import { DocumentObserver } from './document_observer';
|
||||
|
||||
export const DocumentControllerContext = createContext<DocumentController | null>(null);
|
||||
|
||||
export class DocumentController {
|
||||
private readonly backendService: DocumentBackendService;
|
||||
private readonly observer: DocumentObserver;
|
||||
|
||||
constructor(public readonly viewId: string) {
|
||||
this.backendService = new DocumentBackendService(viewId);
|
||||
this.observer = new DocumentObserver(viewId);
|
||||
}
|
||||
|
||||
open = async (): Promise<DocumentData | null> => {
|
||||
const openDocumentResult = await this.backendService.open();
|
||||
if (openDocumentResult.ok) {
|
||||
return {
|
||||
rootId: '',
|
||||
blocks: {},
|
||||
meta: {
|
||||
childrenMap: {},
|
||||
open = async (): Promise<DocumentData | FlowyError> => {
|
||||
// example:
|
||||
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),
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
});
|
||||
const childrenMap: Record<string, string[]> = {};
|
||||
document.val.meta.children_map.forEach((child, key) => { childrenMap[key] = child.children; });
|
||||
return {
|
||||
rootId: document.val.page_id,
|
||||
blocks,
|
||||
meta: {
|
||||
childrenMap
|
||||
}
|
||||
}
|
||||
}
|
||||
return document.val;
|
||||
|
||||
};
|
||||
|
||||
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 { useEffect, useState } from 'react';
|
||||
import { Board } from '../components/board/Board';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
|
||||
export const BoardPage = () => {
|
||||
const params = useParams();
|
||||
const [viewId, setViewId] = useState('');
|
||||
const pagesStore = useAppSelector((state) => state.pages);
|
||||
const [title, setTitle] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (params?.id?.length) {
|
||||
setViewId(params.id);
|
||||
// setDatabaseId('testDb');
|
||||
setTitle(pagesStore.find((page) => page.id === params.id)?.title || '');
|
||||
}
|
||||
}, [params]);
|
||||
}, [params, pagesStore]);
|
||||
|
||||
return (
|
||||
<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} />}
|
||||
{viewId?.length && <Board viewId={viewId} title={title} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -23,7 +23,7 @@ export const useDocument = () => {
|
||||
const res = await c.open();
|
||||
console.log(res)
|
||||
if (!res) return;
|
||||
setDocumentData(res)
|
||||
// setDocumentData(res)
|
||||
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 { useGrid } from './GridPage.hooks';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Grid } from '../components/grid/Grid/Grid';
|
||||
|
||||
export const GridPage = () => {
|
||||
const params = useParams();
|
||||
const { loadGrid } = useGrid();
|
||||
const [viewId, setViewId] = useState('');
|
||||
useEffect(() => {
|
||||
void (async () => {
|
||||
if (!params?.id) return;
|
||||
await loadGrid(params.id);
|
||||
})();
|
||||
if (params?.id?.length) {
|
||||
setViewId(params.id);
|
||||
// setDatabaseId('testDb');
|
||||
}
|
||||
}, [params]);
|
||||
|
||||
return (
|
||||
<div className='mx-auto mt-8 flex flex-col gap-8 px-8'>
|
||||
<h1 className='text-4xl font-bold'>Grid</h1>
|
||||
|
||||
<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 className='flex h-full flex-col gap-8 px-8 pt-8'>
|
||||
<h1 className='text-4xl font-bold'>Grid: {viewId}</h1>
|
||||
{viewId?.length && <Grid viewId={viewId} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -2,5 +2,6 @@ export * from "./models/flowy-user";
|
||||
export * from "./models/flowy-document";
|
||||
export * from "./models/flowy-database";
|
||||
export * from "./models/flowy-folder2";
|
||||
export * from "./models/flowy-document2";
|
||||
export * from "./models/flowy-net";
|
||||
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