fix: modify blocks pb

This commit is contained in:
qinluhe 2023-04-13 20:30:28 +08:00
commit 4582413e89
120 changed files with 3960 additions and 1602 deletions

View File

@ -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),
);
}

View File

@ -3,6 +3,7 @@ import 'package:appflowy/plugins/document/presentation/plugins/cover/cover_node_
import 'package:appflowy/plugins/trash/application/trash_service.dart'; import 'package:appflowy/plugins/trash/application/trash_service.dart';
import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy/workspace/application/view/view_listener.dart'; import 'package:appflowy/workspace/application/view/view_listener.dart';
import 'package:appflowy/workspace/application/doc/doc_listener.dart';
import 'package:appflowy/plugins/document/application/doc_service.dart'; import 'package:appflowy/plugins/document/application/doc_service.dart';
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pbserver.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pbserver.dart';
@ -17,12 +18,13 @@ import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'dart:async'; import 'dart:async';
import 'package:appflowy/util/either_extension.dart'; import 'package:appflowy/util/either_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-document2/entities.pb.dart';
part 'doc_bloc.freezed.dart'; part 'doc_bloc.freezed.dart';
class DocumentBloc extends Bloc<DocumentEvent, DocumentState> { class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
final ViewPB view; final ViewPB view;
final DocumentService _documentService; final DocumentService _documentService;
final DocumentListener _docListener;
final ViewListener _listener; final ViewListener _listener;
final TrashService _trashService; final TrashService _trashService;
@ -32,12 +34,14 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
DocumentBloc({ DocumentBloc({
required this.view, required this.view,
}) : _documentService = DocumentService(), }) : _documentService = DocumentService(),
_docListener = DocumentListener(id: view.id),
_listener = ViewListener(view: view), _listener = ViewListener(view: view),
_trashService = TrashService(), _trashService = TrashService(),
super(DocumentState.initial()) { super(DocumentState.initial()) {
on<DocumentEvent>((event, emit) async { on<DocumentEvent>((event, emit) async {
await event.map( await event.map(
initial: (Initial value) async { initial: (Initial value) async {
_listenOnDocChange();
await _initial(value, emit); await _initial(value, emit);
_listenOnViewChange(); _listenOnViewChange();
}, },
@ -73,6 +77,7 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
} }
await _documentService.closeDocument(docId: view.id); await _documentService.closeDocument(docId: view.id);
await _documentService.closeDocumentV2(view: view);
return super.close(); return super.close();
} }
@ -88,6 +93,39 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
); );
} }
final result = await _documentService.openDocument(view: view); final result = await _documentService.openDocument(view: view);
// test code
final document = await _documentService.openDocumentV2(view: view);
BlockPB? root;
document.fold((l) {
print('---------<open document v2>-----------');
print('page id = ${l.pageId}');
l.blocks.blocks.forEach((key, value) {
print('-----<block begin>-----');
print('block = $value');
if (value.ty == 'page') {
root = value;
}
print('-----<block end>-----');
});
print('---------<open document v2>-----------');
}, (r) {});
if (root != null) {
await _documentService.applyAction(
view: view,
actions: [
BlockActionPB(
action: BlockActionTypePB.Insert,
payload: BlockActionPayloadPB(
block: BlockPB()
..id = 'id_0'
..ty = 'text'
..parentId = root!.id,
),
),
],
);
}
return result.fold( return result.fold(
(documentData) async { (documentData) async {
await _initEditorState(documentData).whenComplete(() { await _initEditorState(documentData).whenComplete(() {
@ -126,6 +164,14 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
); );
} }
void _listenOnDocChange() {
_docListener.start(
didReceiveUpdate: () {
print('---------<receive document update>-----------');
},
);
}
Future<void> _initEditorState(DocumentDataPB documentData) async { Future<void> _initEditorState(DocumentDataPB documentData) async {
final document = Document.fromJson(jsonDecode(documentData.content)); final document = Document.fromJson(jsonDecode(documentData.content));
final editorState = EditorState(document: document); final editorState = EditorState(document: document);

View File

@ -4,6 +4,7 @@ import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-document2/entities.pb.dart';
class DocumentService { class DocumentService {
Future<Either<DocumentDataPB, FlowyError>> openDocument({ Future<Either<DocumentDataPB, FlowyError>> openDocument({
@ -39,4 +40,32 @@ class DocumentService {
final payload = ViewIdPB(value: docId); final payload = ViewIdPB(value: docId);
return FolderEventCloseView(payload).send(); return FolderEventCloseView(payload).send();
} }
Future<Either<DocumentDataPB2, FlowyError>> openDocumentV2({
required ViewPB view,
}) async {
await FolderEventSetLatestView(ViewIdPB(value: view.id)).send();
final payload = OpenDocumentPayloadPBV2()..documentId = view.id;
return DocumentEvent2OpenDocument(payload).send();
}
Future<Either<Unit, FlowyError>> closeDocumentV2({
required ViewPB view,
}) async {
final payload = CloseDocumentPayloadPBV2()..documentId = view.id;
return DocumentEvent2CloseDocument(payload).send();
}
Future<Either<Unit, FlowyError>> applyAction({
required ViewPB view,
required List<BlockActionPB> actions,
}) async {
final payload = ApplyActionPayloadPBV2(
documentId: view.id,
actions: actions,
);
return DocumentEvent2ApplyAction(payload).send();
}
} }

View File

@ -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();
}
}

View File

@ -17,6 +17,7 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_backend/protobuf/dart-ffi/protobuf.dart'; import 'package:appflowy_backend/protobuf/dart-ffi/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart';
// ignore: unused_import // ignore: unused_import
@ -30,6 +31,7 @@ part 'dart_event/flowy-net/dart_event.dart';
part 'dart_event/flowy-user/dart_event.dart'; part 'dart_event/flowy-user/dart_event.dart';
part 'dart_event/flowy-database/dart_event.dart'; part 'dart_event/flowy-database/dart_event.dart';
part 'dart_event/flowy-document/dart_event.dart'; part 'dart_event/flowy-document/dart_event.dart';
part 'dart_event/flowy-document2/dart_event.dart';
enum FFIException { enum FFIException {
RequestIsEmpty, RequestIsEmpty,

File diff suppressed because it is too large Load Diff

View File

@ -36,9 +36,13 @@ custom-protocol = ["tauri/custom-protocol"]
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab" } collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab" } collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab" }
collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab" } collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab" }
#collab = { path = "../../AppFlowy-Collab/collab" } #collab = { path = "../../AppFlowy-Collab/collab" }
#collab-folder = { path = "../../AppFlowy-Collab/collab-folder" } #collab-folder = { path = "../../AppFlowy-Collab/collab-folder" }
#collab-persistence = { path = "../../AppFlowy-Collab/collab-persistence" } #collab-persistence = { path = "../../AppFlowy-Collab/collab-persistence" }
#collab-document = { path = "../../AppFlowy-Collab/collab-document" }

View File

@ -1,6 +1,6 @@
import { Routes, Route, BrowserRouter } from 'react-router-dom'; import { Routes, Route, BrowserRouter } from 'react-router-dom';
import { TestColors } from './components/TestColors/TestColors'; import { ColorPalette } from './components/tests/ColorPalette';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { store } from './stores/store'; import { store } from './stores/store';
import { DocumentPage } from './views/DocumentPage'; import { DocumentPage } from './views/DocumentPage';
@ -14,6 +14,8 @@ import { ErrorHandlerPage } from './components/error/ErrorHandlerPage';
import initializeI18n from './stores/i18n/initializeI18n'; import initializeI18n from './stores/i18n/initializeI18n';
import { TestAPI } from './components/tests/TestAPI'; import { TestAPI } from './components/tests/TestAPI';
import { GetStarted } from './components/auth/GetStarted/GetStarted'; import { GetStarted } from './components/auth/GetStarted/GetStarted';
import { ErrorBoundary } from 'react-error-boundary';
import { AllIcons } from '$app/components/tests/AllIcons';
initializeI18n(); initializeI18n();
@ -21,20 +23,22 @@ const App = () => {
return ( return (
<BrowserRouter> <BrowserRouter>
<Provider store={store}> <Provider store={store}>
<Routes> <ErrorBoundary FallbackComponent={ErrorHandlerPage}>
<Route path={'/'} element={<ProtectedRoutes />}> <Routes>
<Route path={'/page/colors'} element={<TestColors />} /> <Route path={'/'} element={<ProtectedRoutes />}>
<Route path={'/page/api-test'} element={<TestAPI />} /> <Route path={'/page/all-icons'} element={<AllIcons />} />
<Route path={'/page/document/:id'} element={<DocumentPage />} /> <Route path={'/page/colors'} element={<ColorPalette />} />
<Route path={'/page/board/:id'} element={<BoardPage />} /> <Route path={'/page/api-test'} element={<TestAPI />} />
<Route path={'/page/grid/:id'} element={<GridPage />} /> <Route path={'/page/document/:id'} element={<DocumentPage />} />
</Route> <Route path={'/page/board/:id'} element={<BoardPage />} />
<Route path={'/auth/login'} element={<LoginPage />}></Route> <Route path={'/page/grid/:id'} element={<GridPage />} />
<Route path={'/auth/getStarted'} element={<GetStarted />}></Route> </Route>
<Route path={'/auth/signUp'} element={<SignUpPage />}></Route> <Route path={'/auth/login'} element={<LoginPage />}></Route>
<Route path={'/auth/confirm-account'} element={<ConfirmAccountPage />}></Route> <Route path={'/auth/getStarted'} element={<GetStarted />}></Route>
</Routes> <Route path={'/auth/signUp'} element={<SignUpPage />}></Route>
<ErrorHandlerPage></ErrorHandlerPage> <Route path={'/auth/confirm-account'} element={<ConfirmAccountPage />}></Route>
</Routes>
</ErrorBoundary>
</Provider> </Provider>
</BrowserRouter> </BrowserRouter>
); );

View File

@ -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>
);
};

View File

@ -21,7 +21,7 @@ export const CellOptions = ({
<div <div
ref={ref} ref={ref}
onClick={() => onClick()} onClick={() => onClick()}
className={'flex flex-wrap items-center gap-2 px-4 py-2 text-xs text-black'} className={'flex w-full flex-wrap items-center gap-2 px-4 py-2 text-xs text-black'}
> >
{data?.select_options?.map((option, index) => ( {data?.select_options?.map((option, index) => (
<div className={`${getBgColor(option.color)} rounded px-2 py-0.5`} key={index}> <div className={`${getBgColor(option.color)} rounded px-2 py-0.5`} key={index}>

View File

@ -9,10 +9,10 @@ import { useTranslation } from 'react-i18next';
import { Details2Svg } from '$app/components/_shared/svg/Details2Svg'; import { Details2Svg } from '$app/components/_shared/svg/Details2Svg';
import { CheckmarkSvg } from '$app/components/_shared/svg/CheckmarkSvg'; import { CheckmarkSvg } from '$app/components/_shared/svg/CheckmarkSvg';
import { CloseSvg } from '$app/components/_shared/svg/CloseSvg'; import { CloseSvg } from '$app/components/_shared/svg/CloseSvg';
import useOutsideClick from '$app/components/_shared/useOutsideClick';
import { SelectOptionCellBackendService } from '$app/stores/effects/database/cell/select_option_bd_svc'; import { SelectOptionCellBackendService } from '$app/stores/effects/database/cell/select_option_bd_svc';
import { useAppSelector } from '$app/stores/store'; import { useAppSelector } from '$app/stores/store';
import { ISelectOptionType } from '$app/stores/reducers/database/slice'; import { ISelectOption, ISelectOptionType } from '$app/stores/reducers/database/slice';
import { PopupWindow } from '$app/components/_shared/PopupWindow';
export const CellOptionsPopup = ({ export const CellOptionsPopup = ({
top, top,
@ -21,6 +21,7 @@ export const CellOptionsPopup = ({
cellCache, cellCache,
fieldController, fieldController,
onOutsideClick, onOutsideClick,
openOptionDetail,
}: { }: {
top: number; top: number;
left: number; left: number;
@ -28,27 +29,19 @@ export const CellOptionsPopup = ({
cellCache: CellCache; cellCache: CellCache;
fieldController: FieldController; fieldController: FieldController;
onOutsideClick: () => void; onOutsideClick: () => void;
openOptionDetail: (_left: number, _top: number, _select_option: SelectOptionPB) => void;
}) => { }) => {
const ref = useRef<HTMLDivElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const { t } = useTranslation(''); const { t } = useTranslation('');
const [adjustedTop, setAdjustedTop] = useState(-100);
const [value, setValue] = useState(''); const [value, setValue] = useState('');
const { data, cellController } = useCell(cellIdentifier, cellCache, fieldController); const { data } = useCell(cellIdentifier, cellCache, fieldController);
const databaseStore = useAppSelector((state) => state.database); const databaseStore = useAppSelector((state) => state.database);
useEffect(() => { useEffect(() => {
if (!ref.current) return; if (inputRef?.current) {
const { height } = ref.current.getBoundingClientRect(); inputRef.current.focus();
if (top + height + 40 > window.innerHeight) {
setAdjustedTop(window.innerHeight - height - 40);
} else {
setAdjustedTop(top);
} }
}, [ref, window, top, left]); }, [inputRef]);
useOutsideClick(ref, async () => {
onOutsideClick();
});
const onKeyDown: KeyboardEventHandler = async (e) => { const onKeyDown: KeyboardEventHandler = async (e) => {
if (e.key === 'Enter' && value.length > 0) { if (e.key === 'Enter' && value.length > 0) {
@ -63,11 +56,7 @@ export const CellOptionsPopup = ({
}; };
const onToggleOptionClick = async (option: SelectOptionPB) => { const onToggleOptionClick = async (option: SelectOptionPB) => {
if ( if ((data as SelectOptionCellDataPB)?.select_options?.find((selectedOption) => selectedOption.id === option.id)) {
(data as SelectOptionCellDataPB | undefined)?.select_options?.find(
(selectedOption) => selectedOption.id === option.id
)
) {
await new SelectOptionCellBackendService(cellIdentifier).unselectOption([option.id]); await new SelectOptionCellBackendService(cellIdentifier).unselectOption([option.id]);
} else { } else {
await new SelectOptionCellBackendService(cellIdentifier).selectOption([option.id]); await new SelectOptionCellBackendService(cellIdentifier).selectOption([option.id]);
@ -75,23 +64,37 @@ export const CellOptionsPopup = ({
setValue(''); setValue('');
}; };
useEffect(() => { const onKeyDownWrapper: KeyboardEventHandler = (e) => {
console.log('loaded data: ', data); if (e.key === 'Escape') {
console.log('have stored ', databaseStore.fields[cellIdentifier.fieldId]); onOutsideClick();
}, [data]); }
};
const onOptionDetailClick = (e: any, option: ISelectOption) => {
e.stopPropagation();
let target = e.target as HTMLElement;
while (!(target instanceof HTMLButtonElement)) {
if (target.parentElement === null) return;
target = target.parentElement;
}
const selectOption = new SelectOptionPB({
id: option.selectOptionId,
name: option.title,
color: option.color || SelectOptionColorPB.Purple,
});
const { right: _left, top: _top } = target.getBoundingClientRect();
openOptionDetail(_left, _top, selectOption);
};
return ( return (
<div <PopupWindow className={'p-2 text-xs'} onOutsideClick={onOutsideClick} left={left} top={top}>
ref={ref} <div onKeyDown={onKeyDownWrapper} className={'flex flex-col gap-2 p-2'}>
className={`fixed z-10 rounded-lg bg-white px-2 py-2 text-xs shadow-md transition-opacity duration-300 ${
adjustedTop === -100 ? 'opacity-0' : 'opacity-100'
}`}
style={{ top: `${adjustedTop + 40}px`, left: `${left}px` }}
>
<div className={'flex flex-col gap-2 p-2'}>
<div className={'border-shades-3 flex flex-1 items-center gap-2 rounded border bg-main-selector px-2 '}> <div className={'border-shades-3 flex flex-1 items-center gap-2 rounded border bg-main-selector px-2 '}>
<div className={'flex flex-wrap items-center gap-2 text-black'}> <div className={'flex flex-wrap items-center gap-2 text-black'}>
{(data as SelectOptionCellDataPB | undefined)?.select_options?.map((option, index) => ( {(data as SelectOptionCellDataPB)?.select_options?.map((option, index) => (
<div className={`${getBgColor(option.color)} flex items-center gap-0.5 rounded px-1 py-0.5`} key={index}> <div className={`${getBgColor(option.color)} flex items-center gap-0.5 rounded px-1 py-0.5`} key={index}>
<span>{option?.name || ''}</span> <span>{option?.name || ''}</span>
<button onClick={() => onUnselectOptionClick(option)} className={'h-5 w-5 cursor-pointer'}> <button onClick={() => onUnselectOptionClick(option)} className={'h-5 w-5 cursor-pointer'}>
@ -101,6 +104,7 @@ export const CellOptionsPopup = ({
)) || ''} )) || ''}
</div> </div>
<input <input
ref={inputRef}
className={'py-2'} className={'py-2'}
value={value} value={value}
onChange={(e) => setValue(e.target.value)} onChange={(e) => setValue(e.target.value)}
@ -110,7 +114,7 @@ export const CellOptionsPopup = ({
<div className={'font-mono text-shade-3'}>{value.length}/30</div> <div className={'font-mono text-shade-3'}>{value.length}/30</div>
</div> </div>
<div className={'-mx-4 h-[1px] bg-shade-6'}></div> <div className={'-mx-4 h-[1px] bg-shade-6'}></div>
<div className={'font-semibold text-shade-3'}>{t('grid.selectOption.panelTitle') || ''}</div> <div className={'font-medium text-shade-3'}>{t('grid.selectOption.panelTitle') || ''}</div>
<div className={'flex flex-col gap-1'}> <div className={'flex flex-col gap-1'}>
{(databaseStore.fields[cellIdentifier.fieldId]?.fieldOptions as ISelectOptionType).selectOptions.map( {(databaseStore.fields[cellIdentifier.fieldId]?.fieldOptions as ISelectOptionType).selectOptions.map(
(option, index) => ( (option, index) => (
@ -131,14 +135,14 @@ export const CellOptionsPopup = ({
> >
<div className={`${getBgColor(option.color)} rounded px-2 py-0.5`}>{option.title}</div> <div className={`${getBgColor(option.color)} rounded px-2 py-0.5`}>{option.title}</div>
<div className={'flex items-center'}> <div className={'flex items-center'}>
{(data as SelectOptionCellDataPB | undefined)?.select_options?.find( {(data as SelectOptionCellDataPB)?.select_options?.find(
(selectedOption) => selectedOption.id === option.selectOptionId (selectedOption) => selectedOption.id === option.selectOptionId
) && ( ) && (
<button className={'h-5 w-5 p-1'}> <button className={'h-5 w-5 p-1'}>
<CheckmarkSvg></CheckmarkSvg> <CheckmarkSvg></CheckmarkSvg>
</button> </button>
)} )}
<button className={'h-6 w-6 p-1'}> <button onClick={(e) => onOptionDetailClick(e, option)} className={'h-6 w-6 p-1'}>
<Details2Svg></Details2Svg> <Details2Svg></Details2Svg>
</button> </button>
</div> </div>
@ -147,6 +151,6 @@ export const CellOptionsPopup = ({
)} )}
</div> </div>
</div> </div>
</div> </PopupWindow>
); );
}; };

View File

@ -1,8 +1,7 @@
import { FieldType } from '@/services/backend'; import { FieldType } from '@/services/backend';
import { FieldTypeIcon } from '$app/components/_shared/EditRow/FieldTypeIcon'; import { FieldTypeIcon } from '$app/components/_shared/EditRow/FieldTypeIcon';
import { FieldTypeName } from '$app/components/_shared/EditRow/FieldTypeName'; import { FieldTypeName } from '$app/components/_shared/EditRow/FieldTypeName';
import { useEffect, useMemo, useRef, useState } from 'react'; import { PopupWindow } from '$app/components/_shared/PopupWindow';
import useOutsideClick from '$app/components/_shared/useOutsideClick';
const typesOrder: FieldType[] = [ const typesOrder: FieldType[] = [
FieldType.RichText, FieldType.RichText,
@ -17,39 +16,17 @@ const typesOrder: FieldType[] = [
export const ChangeFieldTypePopup = ({ export const ChangeFieldTypePopup = ({
top, top,
right, left,
onClick, onClick,
onOutsideClick, onOutsideClick,
}: { }: {
top: number; top: number;
right: number; left: number;
onClick: (newType: FieldType) => void; onClick: (newType: FieldType) => void;
onOutsideClick: () => void; onOutsideClick: () => void;
}) => { }) => {
const ref = useRef<HTMLDivElement>(null);
const [adjustedTop, setAdjustedTop] = useState(-100);
useOutsideClick(ref, async () => {
onOutsideClick();
});
useEffect(() => {
if (!ref.current) return;
const { height } = ref.current.getBoundingClientRect();
if (top + height > window.innerHeight) {
setAdjustedTop(window.innerHeight - height);
} else {
setAdjustedTop(top);
}
}, [ref, window, top, right]);
return ( return (
<div <PopupWindow className={'p-2 text-xs'} onOutsideClick={onOutsideClick} left={left} top={top}>
ref={ref}
className={`fixed z-10 rounded-lg bg-white p-2 text-xs shadow-md transition-opacity duration-300 ${
adjustedTop === -100 ? 'opacity-0' : 'opacity-100'
}`}
style={{ top: `${adjustedTop}px`, left: `${right + 30}px` }}
>
<div className={'flex flex-col'}> <div className={'flex flex-col'}>
{typesOrder.map((t, i) => ( {typesOrder.map((t, i) => (
<button <button
@ -66,6 +43,6 @@ export const ChangeFieldTypePopup = ({
</button> </button>
))} ))}
</div> </div>
</div> </PopupWindow>
); );
}; };

View File

@ -1,15 +1,17 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc'; import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
import { CellCache } from '$app/stores/effects/database/cell/cell_cache'; import { CellCache } from '$app/stores/effects/database/cell/cell_cache';
import { FieldController } from '$app/stores/effects/database/field/field_controller'; import { FieldController } from '$app/stores/effects/database/field/field_controller';
import useOutsideClick from '$app/components/_shared/useOutsideClick';
import Calendar from 'react-calendar'; import Calendar from 'react-calendar';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { ClockSvg } from '$app/components/_shared/svg/ClockSvg'; import { ClockSvg } from '$app/components/_shared/svg/ClockSvg';
import { MoreSvg } from '$app/components/_shared/svg/MoreSvg'; import { MoreSvg } from '$app/components/_shared/svg/MoreSvg';
import { EditorUncheckSvg } from '$app/components/_shared/svg/EditorUncheckSvg'; import { EditorUncheckSvg } from '$app/components/_shared/svg/EditorUncheckSvg';
import { useCell } from '$app/components/_shared/database-hooks/useCell'; import { useCell } from '$app/components/_shared/database-hooks/useCell';
import { CalendarData } from '$app/stores/effects/database/cell/controller_builder';
import { DateCellDataPB } from '@/services/backend';
import { PopupWindow } from '$app/components/_shared/PopupWindow';
export const DatePickerPopup = ({ export const DatePickerPopup = ({
left, left,
@ -27,47 +29,27 @@ export const DatePickerPopup = ({
onOutsideClick: () => void; onOutsideClick: () => void;
}) => { }) => {
const { data, cellController } = useCell(cellIdentifier, cellCache, fieldController); const { data, cellController } = useCell(cellIdentifier, cellCache, fieldController);
const ref = useRef<HTMLDivElement>(null);
const [adjustedTop, setAdjustedTop] = useState(-100);
// const [value, setValue] = useState();
const { t } = useTranslation(''); const { t } = useTranslation('');
const [selectedDate, setSelectedDate] = useState<Date>(new Date()); const [selectedDate, setSelectedDate] = useState<Date>(new Date());
useEffect(() => { useEffect(() => {
if (!ref.current) return; const date_pb = data as DateCellDataPB | undefined;
const { height } = ref.current.getBoundingClientRect(); if (!date_pb || !date_pb?.date.length) return;
if (top + height + 40 > window.innerHeight) {
setAdjustedTop(top - height - 40);
} else {
setAdjustedTop(top);
}
}, [ref, window, top, left]);
useOutsideClick(ref, async () => { // should be changed after we can modify date format
onOutsideClick(); setSelectedDate(dayjs(date_pb.date, 'MMM DD, YYYY').toDate());
});
useEffect(() => {
// console.log((data as DateCellDataPB).date);
// setSelectedDate(new Date((data as DateCellDataPB).date));
}, [data]); }, [data]);
const onChange = (v: Date | null | (Date | null)[]) => { const onChange = async (v: Date | null | (Date | null)[]) => {
if (v instanceof Date) { if (v instanceof Date) {
console.log(dayjs(v).format('YYYY-MM-DD'));
setSelectedDate(v); setSelectedDate(v);
// void cellController?.saveCellData(new DateCellDataPB({ date: dayjs(v).format('YYYY-MM-DD') })); const date = new CalendarData(dayjs(v).add(dayjs().utcOffset(), 'minutes').toDate(), false);
await cellController?.saveCellData(date);
} }
}; };
return ( return (
<div <PopupWindow className={'p-2 text-xs'} onOutsideClick={onOutsideClick} left={left} top={top}>
ref={ref}
className={`fixed z-10 rounded-lg bg-white px-2 py-2 text-xs shadow-md transition-opacity duration-300 ${
adjustedTop === -100 ? 'opacity-0' : 'opacity-100'
}`}
style={{ top: `${adjustedTop + 40}px`, left: `${left}px` }}
>
<div className={'px-2'}> <div className={'px-2'}>
<Calendar onChange={(d) => onChange(d)} value={selectedDate} /> <Calendar onChange={(d) => onChange(d)} value={selectedDate} />
</div> </div>
@ -92,6 +74,6 @@ export const DatePickerPopup = ({
<MoreSvg></MoreSvg> <MoreSvg></MoreSvg>
</i> </i>
</div> </div>
</div> </PopupWindow>
); );
}; };

View File

@ -17,7 +17,7 @@ export const EditCellDate = ({
}; };
return ( return (
<div ref={ref} onClick={() => onClick()} className={'px-4 py-2'}> <div ref={ref} onClick={() => onClick()} className={'w-full px-4 py-2'}>
{data?.date || <>&nbsp;</>} {data?.date || <>&nbsp;</>}
</div> </div>
); );

View File

@ -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>
);
};

View File

@ -27,9 +27,9 @@ export const EditCellWrapper = ({
cellIdentifier: CellIdentifier; cellIdentifier: CellIdentifier;
cellCache: CellCache; cellCache: CellCache;
fieldController: FieldController; fieldController: FieldController;
onEditFieldClick: (top: number, right: number) => void; onEditFieldClick: (cell: CellIdentifier, left: number, top: number) => void;
onEditOptionsClick: (left: number, top: number) => void; onEditOptionsClick: (cell: CellIdentifier, left: number, top: number) => void;
onEditDateClick: (left: number, top: number) => void; onEditDateClick: (cell: CellIdentifier, left: number, top: number) => void;
}) => { }) => {
const { data, cellController } = useCell(cellIdentifier, cellCache, fieldController); const { data, cellController } = useCell(cellIdentifier, cellCache, fieldController);
const databaseStore = useAppSelector((state) => state.database); const databaseStore = useAppSelector((state) => state.database);
@ -38,7 +38,7 @@ export const EditCellWrapper = ({
const onClick = () => { const onClick = () => {
if (!el.current) return; if (!el.current) return;
const { top, right } = el.current.getBoundingClientRect(); const { top, right } = el.current.getBoundingClientRect();
onEditFieldClick(top, right); onEditFieldClick(cellIdentifier, right, top);
}; };
return ( return (
@ -70,17 +70,23 @@ export const EditCellWrapper = ({
cellIdentifier.fieldType === FieldType.Checklist) && cellIdentifier.fieldType === FieldType.Checklist) &&
cellController && ( cellController && (
<CellOptions <CellOptions
data={data as SelectOptionCellDataPB | undefined} data={data as SelectOptionCellDataPB}
onEditClick={onEditOptionsClick} onEditClick={(left, top) => onEditOptionsClick(cellIdentifier, left, top)}
></CellOptions> ></CellOptions>
)} )}
{cellIdentifier.fieldType === FieldType.Checkbox && cellController && ( {cellIdentifier.fieldType === FieldType.Checkbox && cellController && (
<EditCheckboxCell data={data as boolean | undefined} cellController={cellController}></EditCheckboxCell> <EditCheckboxCell
data={data as 'Yes' | 'No' | undefined}
cellController={cellController}
></EditCheckboxCell>
)} )}
{cellIdentifier.fieldType === FieldType.DateTime && ( {cellIdentifier.fieldType === FieldType.DateTime && (
<EditCellDate data={data as DateCellDataPB | undefined} onEditClick={onEditDateClick}></EditCellDate> <EditCellDate
data={data as DateCellDataPB}
onEditClick={(left, top) => onEditDateClick(cellIdentifier, left, top)}
></EditCellDate>
)} )}
{cellIdentifier.fieldType === FieldType.Number && cellController && ( {cellIdentifier.fieldType === FieldType.Number && cellController && (
@ -88,7 +94,7 @@ export const EditCellWrapper = ({
)} )}
{cellIdentifier.fieldType === FieldType.URL && cellController && ( {cellIdentifier.fieldType === FieldType.URL && cellController && (
<EditCellUrl data={data as URLCellDataPB | undefined} cellController={cellController}></EditCellUrl> <EditCellUrl data={data as URLCellDataPB} cellController={cellController}></EditCellUrl>
)} )}
{cellIdentifier.fieldType === FieldType.RichText && cellController && ( {cellIdentifier.fieldType === FieldType.RichText && cellController && (

View File

@ -1,22 +1,26 @@
import { EditorCheckSvg } from '$app/components/_shared/svg/EditorCheckSvg'; import { EditorCheckSvg } from '$app/components/_shared/svg/EditorCheckSvg';
import { EditorUncheckSvg } from '$app/components/_shared/svg/EditorUncheckSvg'; import { EditorUncheckSvg } from '$app/components/_shared/svg/EditorUncheckSvg';
import { CellController } from '$app/stores/effects/database/cell/cell_controller'; import { CheckboxCellController } from '$app/stores/effects/database/cell/controller_builder';
export const EditCheckboxCell = ({ export const EditCheckboxCell = ({
data, data,
cellController, cellController,
}: { }: {
data: boolean | undefined; data: 'Yes' | 'No' | undefined;
cellController: CellController<any, any>; cellController: CheckboxCellController;
}) => { }) => {
const toggleValue = async () => { const toggleValue = async () => {
await cellController?.saveCellData(!data); if (data === 'Yes') {
await cellController?.saveCellData('No');
} else {
await cellController?.saveCellData('Yes');
}
}; };
return ( return (
<div onClick={() => toggleValue()} className={'block px-4 py-2'}> <div onClick={() => toggleValue()} className={'block px-4 py-2'}>
<button className={'h-5 w-5'}> <button className={'h-5 w-5'}>
{data ? <EditorCheckSvg></EditorCheckSvg> : <EditorUncheckSvg></EditorUncheckSvg>} {data === 'Yes' ? <EditorCheckSvg></EditorCheckSvg> : <EditorUncheckSvg></EditorUncheckSvg>}
</button> </button>
</div> </div>
); );

View File

@ -1,5 +1,4 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import useOutsideClick from '$app/components/_shared/useOutsideClick';
import { TrashSvg } from '$app/components/_shared/svg/TrashSvg'; import { TrashSvg } from '$app/components/_shared/svg/TrashSvg';
import { FieldTypeIcon } from '$app/components/_shared/EditRow/FieldTypeIcon'; import { FieldTypeIcon } from '$app/components/_shared/EditRow/FieldTypeIcon';
import { FieldTypeName } from '$app/components/_shared/EditRow/FieldTypeName'; import { FieldTypeName } from '$app/components/_shared/EditRow/FieldTypeName';
@ -10,10 +9,11 @@ import { FieldInfo } from '$app/stores/effects/database/field/field_controller';
import { MoreSvg } from '$app/components/_shared/svg/MoreSvg'; import { MoreSvg } from '$app/components/_shared/svg/MoreSvg';
import { useAppSelector } from '$app/stores/store'; import { useAppSelector } from '$app/stores/store';
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc'; import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
import { PopupWindow } from '$app/components/_shared/PopupWindow';
export const EditFieldPopup = ({ export const EditFieldPopup = ({
top, top,
right, left,
cellIdentifier, cellIdentifier,
viewId, viewId,
onOutsideClick, onOutsideClick,
@ -21,7 +21,7 @@ export const EditFieldPopup = ({
changeFieldTypeClick, changeFieldTypeClick,
}: { }: {
top: number; top: number;
right: number; left: number;
cellIdentifier: CellIdentifier; cellIdentifier: CellIdentifier;
viewId: string; viewId: string;
onOutsideClick: () => void; onOutsideClick: () => void;
@ -30,31 +30,13 @@ export const EditFieldPopup = ({
}) => { }) => {
const databaseStore = useAppSelector((state) => state.database); const databaseStore = useAppSelector((state) => state.database);
const { t } = useTranslation(''); const { t } = useTranslation('');
const ref = useRef<HTMLDivElement>(null);
const changeTypeButtonRef = useRef<HTMLDivElement>(null); const changeTypeButtonRef = useRef<HTMLDivElement>(null);
const [name, setName] = useState(''); const [name, setName] = useState('');
const [adjustedTop, setAdjustedTop] = useState(-100);
useOutsideClick(ref, async () => {
await save();
onOutsideClick();
});
useEffect(() => { useEffect(() => {
setName(databaseStore.fields[cellIdentifier.fieldId].title); setName(databaseStore.fields[cellIdentifier.fieldId].title);
}, [databaseStore, cellIdentifier]); }, [databaseStore, cellIdentifier]);
useEffect(() => {
if (!ref.current) return;
const { height } = ref.current.getBoundingClientRect();
if (top + height > window.innerHeight) {
setAdjustedTop(window.innerHeight - height);
} else {
setAdjustedTop(top);
}
}, [ref, window, top, right]);
const save = async () => { const save = async () => {
if (!fieldInfo) return; if (!fieldInfo) return;
const controller = new TypeOptionController(viewId, Some(fieldInfo)); const controller = new TypeOptionController(viewId, Some(fieldInfo));
@ -78,12 +60,14 @@ export const EditFieldPopup = ({
}; };
return ( return (
<div <PopupWindow
ref={ref} className={'px-2 py-2 text-xs'}
className={`fixed z-10 rounded-lg bg-white px-2 py-2 text-xs shadow-md transition-opacity duration-300 ${ onOutsideClick={async () => {
adjustedTop === -100 ? 'opacity-0' : 'opacity-100' await save();
}`} onOutsideClick();
style={{ top: `${adjustedTop}px`, left: `${right + 10}px` }} }}
left={left}
top={top}
> >
<div className={'flex flex-col gap-2 p-2'}> <div className={'flex flex-col gap-2 p-2'}>
<input <input
@ -125,6 +109,6 @@ export const EditFieldPopup = ({
</i> </i>
</div> </div>
</div> </div>
</div> </PopupWindow>
); );
}; };

View File

@ -11,10 +11,11 @@ import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
import { ChangeFieldTypePopup } from '$app/components/_shared/EditRow/ChangeFieldTypePopup'; import { ChangeFieldTypePopup } from '$app/components/_shared/EditRow/ChangeFieldTypePopup';
import { TypeOptionController } from '$app/stores/effects/database/field/type_option/type_option_controller'; import { TypeOptionController } from '$app/stores/effects/database/field/type_option/type_option_controller';
import { Some } from 'ts-results'; import { Some } from 'ts-results';
import { FieldType } from '@/services/backend'; import { FieldType, SelectOptionPB } from '@/services/backend';
import { CellOptionsPopup } from '$app/components/_shared/EditRow/CellOptionsPopup'; import { CellOptionsPopup } from '$app/components/_shared/EditRow/CellOptionsPopup';
import { DatePickerPopup } from '$app/components/_shared/EditRow/DatePickerPopup'; import { DatePickerPopup } from '$app/components/_shared/EditRow/DatePickerPopup';
import { DragDropContext, Droppable, OnDragEndResponder } from 'react-beautiful-dnd'; import { DragDropContext, Droppable, OnDragEndResponder } from 'react-beautiful-dnd';
import { EditCellOptionPopup } from '$app/components/_shared/EditRow/EditCellOptionPopup';
export const EditRow = ({ export const EditRow = ({
onClose, onClose,
@ -34,11 +35,11 @@ export const EditRow = ({
const [editingCell, setEditingCell] = useState<CellIdentifier | null>(null); const [editingCell, setEditingCell] = useState<CellIdentifier | null>(null);
const [showFieldEditor, setShowFieldEditor] = useState(false); const [showFieldEditor, setShowFieldEditor] = useState(false);
const [editFieldTop, setEditFieldTop] = useState(0); const [editFieldTop, setEditFieldTop] = useState(0);
const [editFieldRight, setEditFieldRight] = useState(0); const [editFieldLeft, setEditFieldLeft] = useState(0);
const [showChangeFieldTypePopup, setShowChangeFieldTypePopup] = useState(false); const [showChangeFieldTypePopup, setShowChangeFieldTypePopup] = useState(false);
const [changeFieldTypeTop, setChangeFieldTypeTop] = useState(0); const [changeFieldTypeTop, setChangeFieldTypeTop] = useState(0);
const [changeFieldTypeRight, setChangeFieldTypeRight] = useState(0); const [changeFieldTypeLeft, setChangeFieldTypeLeft] = useState(0);
const [showChangeOptionsPopup, setShowChangeOptionsPopup] = useState(false); const [showChangeOptionsPopup, setShowChangeOptionsPopup] = useState(false);
const [changeOptionsTop, setChangeOptionsTop] = useState(0); const [changeOptionsTop, setChangeOptionsTop] = useState(0);
@ -48,6 +49,12 @@ export const EditRow = ({
const [datePickerTop, setDatePickerTop] = useState(0); const [datePickerTop, setDatePickerTop] = useState(0);
const [datePickerLeft, setDatePickerLeft] = useState(0); const [datePickerLeft, setDatePickerLeft] = useState(0);
const [showEditCellOption, setShowEditCellOption] = useState(false);
const [editCellOptionTop, setEditCellOptionTop] = useState(0);
const [editCellOptionLeft, setEditCellOptionLeft] = useState(0);
const [editingSelectOption, setEditingSelectOption] = useState<SelectOptionPB | undefined>();
useEffect(() => { useEffect(() => {
setUnveil(true); setUnveil(true);
}, []); }, []);
@ -59,10 +66,10 @@ export const EditRow = ({
}, 300); }, 300);
}; };
const onEditFieldClick = (cellIdentifier: CellIdentifier, top: number, right: number) => { const onEditFieldClick = (cellIdentifier: CellIdentifier, left: number, top: number) => {
setEditingCell(cellIdentifier); setEditingCell(cellIdentifier);
setEditFieldTop(top); setEditFieldTop(top);
setEditFieldRight(right); setEditFieldLeft(left + 10);
setShowFieldEditor(true); setShowFieldEditor(true);
}; };
@ -74,7 +81,7 @@ export const EditRow = ({
const onChangeFieldTypeClick = (buttonTop: number, buttonRight: number) => { const onChangeFieldTypeClick = (buttonTop: number, buttonRight: number) => {
setChangeFieldTypeTop(buttonTop); setChangeFieldTypeTop(buttonTop);
setChangeFieldTypeRight(buttonRight); setChangeFieldTypeLeft(buttonRight + 30);
setShowChangeFieldTypePopup(true); setShowChangeFieldTypePopup(true);
}; };
@ -95,17 +102,24 @@ export const EditRow = ({
const onEditOptionsClick = async (cellIdentifier: CellIdentifier, left: number, top: number) => { const onEditOptionsClick = async (cellIdentifier: CellIdentifier, left: number, top: number) => {
setEditingCell(cellIdentifier); setEditingCell(cellIdentifier);
setChangeOptionsLeft(left); setChangeOptionsLeft(left);
setChangeOptionsTop(top); setChangeOptionsTop(top + 40);
setShowChangeOptionsPopup(true); setShowChangeOptionsPopup(true);
}; };
const onEditDateClick = async (cellIdentifier: CellIdentifier, left: number, top: number) => { const onEditDateClick = async (cellIdentifier: CellIdentifier, left: number, top: number) => {
setEditingCell(cellIdentifier); setEditingCell(cellIdentifier);
setDatePickerLeft(left); setDatePickerLeft(left);
setDatePickerTop(top); setDatePickerTop(top + 40);
setShowDatePicker(true); setShowDatePicker(true);
}; };
const onOpenOptionDetailClick = (_left: number, _top: number, _select_option: SelectOptionPB) => {
setEditingSelectOption(_select_option);
setShowEditCellOption(true);
setEditCellOptionLeft(_left);
setEditCellOptionTop(_top);
};
const onDragEnd: OnDragEndResponder = (result) => { const onDragEnd: OnDragEndResponder = (result) => {
if (!result.destination?.index) return; if (!result.destination?.index) return;
void controller.moveField({ void controller.moveField({
@ -120,8 +134,14 @@ export const EditRow = ({
className={`fixed inset-0 z-10 flex items-center justify-center bg-black/30 backdrop-blur-sm transition-opacity duration-300 ${ className={`fixed inset-0 z-10 flex items-center justify-center bg-black/30 backdrop-blur-sm transition-opacity duration-300 ${
unveil ? 'opacity-100' : 'opacity-0' unveil ? 'opacity-100' : 'opacity-0'
}`} }`}
onClick={() => onCloseClick()}
> >
<div className={`relative flex h-[90%] w-[70%] flex-col gap-8 rounded-xl bg-white px-8 pb-4 pt-12`}> <div
onClick={(e) => {
e.stopPropagation();
}}
className={`relative flex h-[90%] w-[70%] flex-col gap-8 rounded-xl bg-white px-8 pb-4 pt-12`}
>
<div onClick={() => onCloseClick()} className={'absolute top-4 right-4'}> <div onClick={() => onCloseClick()} className={'absolute top-4 right-4'}>
<button className={'block h-8 w-8 rounded-lg text-shade-2 hover:bg-main-secondary'}> <button className={'block h-8 w-8 rounded-lg text-shade-2 hover:bg-main-secondary'}>
<CloseSvg></CloseSvg> <CloseSvg></CloseSvg>
@ -145,11 +165,9 @@ export const EditRow = ({
cellIdentifier={cell.cellIdentifier} cellIdentifier={cell.cellIdentifier}
cellCache={controller.databaseViewCache.getRowCache().getCellCache()} cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
fieldController={controller.fieldController} fieldController={controller.fieldController}
onEditFieldClick={(top: number, right: number) => onEditFieldClick(cell.cellIdentifier, top, right)} onEditFieldClick={onEditFieldClick}
onEditOptionsClick={(left: number, top: number) => onEditOptionsClick={onEditOptionsClick}
onEditOptionsClick(cell.cellIdentifier, left, top) onEditDateClick={onEditDateClick}
}
onEditDateClick={(left: number, top: number) => onEditDateClick(cell.cellIdentifier, left, top)}
></EditCellWrapper> ></EditCellWrapper>
))} ))}
</div> </div>
@ -172,7 +190,7 @@ export const EditRow = ({
{showFieldEditor && editingCell && ( {showFieldEditor && editingCell && (
<EditFieldPopup <EditFieldPopup
top={editFieldTop} top={editFieldTop}
right={editFieldRight} left={editFieldLeft}
cellIdentifier={editingCell} cellIdentifier={editingCell}
viewId={viewId} viewId={viewId}
onOutsideClick={onOutsideEditFieldClick} onOutsideClick={onOutsideEditFieldClick}
@ -183,7 +201,7 @@ export const EditRow = ({
{showChangeFieldTypePopup && ( {showChangeFieldTypePopup && (
<ChangeFieldTypePopup <ChangeFieldTypePopup
top={changeFieldTypeTop} top={changeFieldTypeTop}
right={changeFieldTypeRight} left={changeFieldTypeLeft}
onClick={(newType) => changeFieldType(newType)} onClick={(newType) => changeFieldType(newType)}
onOutsideClick={() => setShowChangeFieldTypePopup(false)} onOutsideClick={() => setShowChangeFieldTypePopup(false)}
></ChangeFieldTypePopup> ></ChangeFieldTypePopup>
@ -196,6 +214,7 @@ export const EditRow = ({
cellCache={controller.databaseViewCache.getRowCache().getCellCache()} cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
fieldController={controller.fieldController} fieldController={controller.fieldController}
onOutsideClick={() => setShowChangeOptionsPopup(false)} onOutsideClick={() => setShowChangeOptionsPopup(false)}
openOptionDetail={onOpenOptionDetailClick}
></CellOptionsPopup> ></CellOptionsPopup>
)} )}
{showDatePicker && editingCell && ( {showDatePicker && editingCell && (
@ -208,6 +227,17 @@ export const EditRow = ({
onOutsideClick={() => setShowDatePicker(false)} onOutsideClick={() => setShowDatePicker(false)}
></DatePickerPopup> ></DatePickerPopup>
)} )}
{showEditCellOption && editingCell && editingSelectOption && (
<EditCellOptionPopup
top={editCellOptionTop}
left={editCellOptionLeft}
cellIdentifier={editingCell}
editingSelectOption={editingSelectOption}
onOutsideClick={() => {
setShowEditCellOption(false);
}}
></EditCellOptionPopup>
)}
</div> </div>
</div> </div>
); );

View File

@ -1,4 +1,4 @@
import { IPopupItem, Popup } from './Popup'; import { IPopupItem, PopupSelect } from './PopupSelect';
import i18n from 'i18next'; import i18n from 'i18next';
const supportedLanguages: { key: string; title: string }[] = [ const supportedLanguages: { key: string; title: string }[] = [
@ -37,11 +37,11 @@ export const LanguageSelectPopup = ({ onClose }: { onClose: () => void }) => {
icon: <></>, icon: <></>,
})); }));
return ( return (
<Popup <PopupSelect
items={items} items={items}
className={'absolute top-full right-0 z-10 w-[200px]'} className={'absolute top-full right-0 z-10 w-[200px]'}
onOutsideClick={onClose} onOutsideClick={onClose}
columns={2} columns={2}
></Popup> ></PopupSelect>
); );
}; };

View File

@ -2,12 +2,12 @@ import { MouseEvent, ReactNode, useRef } from 'react';
import useOutsideClick from './useOutsideClick'; import useOutsideClick from './useOutsideClick';
export interface IPopupItem { export interface IPopupItem {
icon: ReactNode; icon: ReactNode | (() => JSX.Element);
title: string; title: string;
onClick: () => void; onClick: () => void;
} }
export const Popup = ({ export const PopupSelect = ({
items, items,
className = '', className = '',
onOutsideClick, onOutsideClick,
@ -31,18 +31,20 @@ export const Popup = ({
return ( return (
<div ref={ref} className={`${className} rounded-lg bg-white px-2 py-2 shadow-md`} style={style}> <div ref={ref} className={`${className} rounded-lg bg-white px-2 py-2 shadow-md`} style={style}>
<div <div
className={`grid ${columns === 1 && 'grid-cols-1'} ${columns === 2 && 'grid-cols-2'} ${ className={
columns === 3 && 'grid-cols-3' (columns === 2 ? 'grid grid-cols-2' : '') + (columns === 3 ? 'grid grid-cols-3' : '') + ' w-full gap-x-4'
} gap-x-4`} }
> >
{items.map((item, index) => ( {items.map((item, index) => (
<button <button
key={index} key={index}
className={'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 hover:bg-main-secondary'} className={'flex w-full cursor-pointer items-center gap-2 rounded-lg px-2 py-2 hover:bg-main-secondary'}
onClick={(e) => handleClick(e, item)} onClick={(e) => handleClick(e, item)}
> >
{item.icon} <>
<span className={'flex-shrink-0'}>{item.title}</span> {typeof item.icon === 'function' ? item.icon() : item.icon}
<span className={'flex-shrink-0'}>{item.title}</span>
</>
</button> </button>
))} ))}
</div> </div>

View File

@ -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>
);
};

View File

@ -56,7 +56,15 @@ export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
void loadFields(fieldInfos); void loadFields(fieldInfos);
}, },
}); });
await controller.open();
const openResult = await controller.open();
if (openResult.ok) {
setRows(
openResult.val.map((pb) => {
return new RowInfo(viewId, controller.fieldController.fieldInfos, pb);
})
);
}
if (type === ViewLayoutPB.Board) { if (type === ViewLayoutPB.Board) {
const fieldId = await controller.getGroupByFieldId(); const fieldId = await controller.getGroupByFieldId();

View File

@ -1,4 +1,4 @@
export const EyeClosed = () => { export const EyeClosedSvg = () => {
return ( return (
<svg width='100%' height='100%' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'> <svg width='100%' height='100%' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path <path

View File

@ -1,4 +1,4 @@
export const EyeOpened = () => { export const EyeOpenSvg = () => {
return ( return (
<svg width='100%' height='100%' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'> <svg width='100%' height='100%' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path <path

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -1,6 +1,6 @@
import { AppflowyLogo } from '../../_shared/svg/AppflowyLogo'; import { AppflowyLogo } from '../../_shared/svg/AppflowyLogo';
import { EyeClosed } from '../../_shared/svg/EyeClosedSvg'; import { EyeClosedSvg } from '../../_shared/svg/EyeClosedSvg';
import { EyeOpened } from '../../_shared/svg/EyeOpenSvg'; import { EyeOpenSvg } from '../../_shared/svg/EyeOpenSvg';
import { useLogin } from './Login.hooks'; import { useLogin } from './Login.hooks';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Button } from '../../_shared/Button'; import { Button } from '../../_shared/Button';
@ -55,7 +55,7 @@ export const Login = () => {
className='absolute right-0 top-0 flex h-full w-12 items-center justify-center ' className='absolute right-0 top-0 flex h-full w-12 items-center justify-center '
onClick={onTogglePassword} onClick={onTogglePassword}
> >
<span className='h-6 w-6'>{showPassword ? <EyeClosed /> : <EyeOpened />}</span> <span className='h-6 w-6'>{showPassword ? <EyeClosedSvg /> : <EyeOpenSvg />}</span>
</button> </button>
</div> </div>

View File

@ -1,6 +1,6 @@
import { AppflowyLogo } from '../../_shared/svg/AppflowyLogo'; import { AppflowyLogo } from '../../_shared/svg/AppflowyLogo';
import { EyeClosed } from '../../_shared/svg/EyeClosedSvg'; import { EyeClosedSvg } from '../../_shared/svg/EyeClosedSvg';
import { EyeOpened } from '../../_shared/svg/EyeOpenSvg'; import { EyeOpenSvg } from '../../_shared/svg/EyeOpenSvg';
import { useSignUp } from './SignUp.hooks'; import { useSignUp } from './SignUp.hooks';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
@ -71,7 +71,7 @@ export const SignUp = () => {
onClick={onTogglePassword} onClick={onTogglePassword}
type='button' type='button'
> >
<span className='h-6 w-6'>{showPassword ? <EyeClosed /> : <EyeOpened />}</span> <span className='h-6 w-6'>{showPassword ? <EyeClosedSvg /> : <EyeOpenSvg />}</span>
</button> </button>
</div> </div>
@ -89,7 +89,7 @@ export const SignUp = () => {
onClick={onToggleConfirmPassword} onClick={onToggleConfirmPassword}
type='button' type='button'
> >
<span className='h-6 w-6'>{showConfirmPassword ? <EyeClosed /> : <EyeOpened />}</span> <span className='h-6 w-6'>{showConfirmPassword ? <EyeClosedSvg /> : <EyeOpenSvg />}</span>
</button> </button>
</div> </div>
</div> </div>

View File

@ -1,6 +1,5 @@
import { SettingsSvg } from '../_shared/svg/SettingsSvg';
import { SearchInput } from '../_shared/SearchInput'; import { SearchInput } from '../_shared/SearchInput';
import { BoardBlock } from './BoardBlock'; import { BoardGroup } from './BoardGroup';
import { NewBoardBlock } from './NewBoardBlock'; import { NewBoardBlock } from './NewBoardBlock';
import { useDatabase } from '../_shared/database-hooks/useDatabase'; import { useDatabase } from '../_shared/database-hooks/useDatabase';
import { ViewLayoutPB } from '@/services/backend'; import { ViewLayoutPB } from '@/services/backend';
@ -8,8 +7,9 @@ import { DragDropContext } from 'react-beautiful-dnd';
import { useState } from 'react'; import { useState } from 'react';
import { RowInfo } from '$app/stores/effects/database/row/row_cache'; import { RowInfo } from '$app/stores/effects/database/row/row_cache';
import { EditRow } from '$app/components/_shared/EditRow/EditRow'; import { EditRow } from '$app/components/_shared/EditRow/EditRow';
import { BoardToolbar } from '$app/components/board/BoardToolbar';
export const Board = ({ viewId }: { viewId: string }) => { export const Board = ({ viewId, title }: { viewId: string; title: string }) => {
const { controller, rows, groups, groupByFieldId, onNewRowClick, onDragEnd } = useDatabase(viewId, ViewLayoutPB.Board); const { controller, rows, groups, groupByFieldId, onNewRowClick, onDragEnd } = useDatabase(viewId, ViewLayoutPB.Board);
const [showBoardRow, setShowBoardRow] = useState(false); const [showBoardRow, setShowBoardRow] = useState(false);
const [boardRowInfo, setBoardRowInfo] = useState<RowInfo>(); const [boardRowInfo, setBoardRowInfo] = useState<RowInfo>();
@ -22,12 +22,7 @@ export const Board = ({ viewId }: { viewId: string }) => {
return ( return (
<> <>
<div className='flex w-full items-center justify-between'> <div className='flex w-full items-center justify-between'>
<div className={'flex items-center text-xl font-semibold'}> <BoardToolbar title={title} />
<div>{'Kanban'}</div>
<button className={'ml-2 h-5 w-5'}>
<SettingsSvg></SettingsSvg>
</button>
</div>
<div className='flex shrink-0 items-center gap-4'> <div className='flex shrink-0 items-center gap-4'>
<SearchInput /> <SearchInput />
@ -39,7 +34,7 @@ export const Board = ({ viewId }: { viewId: string }) => {
{controller && {controller &&
groups && groups &&
groups.map((group, index) => ( groups.map((group, index) => (
<BoardBlock <BoardGroup
key={group.groupId} key={group.groupId}
viewId={viewId} viewId={viewId}
controller={controller} controller={controller}

View File

@ -1,9 +1,10 @@
import { Details2Svg } from '../_shared/svg/Details2Svg'; import { Details2Svg } from '../_shared/svg/Details2Svg';
import { RowInfo } from '../../stores/effects/database/row/row_cache'; import { RowInfo } from '$app/stores/effects/database/row/row_cache';
import { useRow } from '../_shared/database-hooks/useRow'; import { useRow } from '../_shared/database-hooks/useRow';
import { DatabaseController } from '../../stores/effects/database/database_controller'; import { DatabaseController } from '$app/stores/effects/database/database_controller';
import { BoardCell } from './BoardCell'; import { BoardCell } from './BoardCell';
import { Draggable } from 'react-beautiful-dnd'; import { Draggable } from 'react-beautiful-dnd';
import { MouseEventHandler } from 'react';
export const BoardCard = ({ export const BoardCard = ({
index, index,
@ -22,6 +23,11 @@ export const BoardCard = ({
}) => { }) => {
const { cells } = useRow(viewId, controller, rowInfo); const { cells } = useRow(viewId, controller, rowInfo);
const onDetailClick: MouseEventHandler = (e) => {
e.stopPropagation();
// onOpenRow(rowInfo);
};
return ( return (
<Draggable draggableId={rowInfo.row.id} index={index}> <Draggable draggableId={rowInfo.row.id} index={index}>
{(provided) => ( {(provided) => (
@ -32,7 +38,7 @@ export const BoardCard = ({
onClick={() => onOpenRow(rowInfo)} onClick={() => onOpenRow(rowInfo)}
className={`relative cursor-pointer select-none rounded-lg border border-shade-6 bg-white px-3 py-2 transition-transform duration-100 hover:bg-main-selector `} className={`relative cursor-pointer select-none rounded-lg border border-shade-6 bg-white px-3 py-2 transition-transform duration-100 hover:bg-main-selector `}
> >
<button className={'absolute right-4 top-2.5 h-5 w-5 rounded hover:bg-surface-2'}> <button onClick={onDetailClick} className={'absolute right-4 top-2.5 h-5 w-5 rounded hover:bg-surface-2'}>
<Details2Svg></Details2Svg> <Details2Svg></Details2Svg>
</button> </button>
<div className={'flex flex-col gap-3'}> <div className={'flex flex-col gap-3'}>

View File

@ -1,11 +1,12 @@
import { CellIdentifier } from '../../stores/effects/database/cell/cell_bd_svc'; import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
import { CellCache } from '../../stores/effects/database/cell/cell_cache'; import { CellCache } from '$app/stores/effects/database/cell/cell_cache';
import { FieldController } from '../../stores/effects/database/field/field_controller'; import { FieldController } from '$app/stores/effects/database/field/field_controller';
import { FieldType } from '../../../services/backend'; import { FieldType } from '@/services/backend';
import { BoardOptionsCell } from './BoardOptionsCell'; import { BoardOptionsCell } from './BoardOptionsCell';
import { BoardDateCell } from './BoardDateCell'; import { BoardDateCell } from './BoardDateCell';
import { BoardTextCell } from './BoardTextCell'; import { BoardTextCell } from './BoardTextCell';
import { BoardUrlCell } from '$app/components/board/BoardUrlCell'; import { BoardUrlCell } from '$app/components/board/BoardUrlCell';
import { BoardCheckboxCell } from '$app/components/board/BoardCheckboxCell';
export const BoardCell = ({ export const BoardCell = ({
cellIdentifier, cellIdentifier,
@ -38,6 +39,12 @@ export const BoardCell = ({
cellCache={cellCache} cellCache={cellCache}
fieldController={fieldController} fieldController={fieldController}
></BoardUrlCell> ></BoardUrlCell>
) : cellIdentifier.fieldType === FieldType.Checkbox ? (
<BoardCheckboxCell
cellIdentifier={cellIdentifier}
cellCache={cellCache}
fieldController={fieldController}
></BoardCheckboxCell>
) : ( ) : (
<BoardTextCell <BoardTextCell
cellIdentifier={cellIdentifier} cellIdentifier={cellIdentifier}

View File

@ -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>
);
};

View File

@ -1,8 +1,8 @@
import { DateCellDataPB } from '../../../services/backend'; import { DateCellDataPB } from '@/services/backend';
import { useCell } from '../_shared/database-hooks/useCell'; import { useCell } from '../_shared/database-hooks/useCell';
import { CellIdentifier } from '../../stores/effects/database/cell/cell_bd_svc'; import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
import { CellCache } from '../../stores/effects/database/cell/cell_cache'; import { CellCache } from '$app/stores/effects/database/cell/cell_cache';
import { FieldController } from '../../stores/effects/database/field/field_controller'; import { FieldController } from '$app/stores/effects/database/field/field_controller';
export const BoardDateCell = ({ export const BoardDateCell = ({
cellIdentifier, cellIdentifier,

View File

@ -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>
);
};

View File

@ -1,12 +1,12 @@
import { Details2Svg } from '../_shared/svg/Details2Svg'; import { Details2Svg } from '../_shared/svg/Details2Svg';
import AddSvg from '../_shared/svg/AddSvg'; import AddSvg from '../_shared/svg/AddSvg';
import { BoardCard } from './BoardCard'; import { BoardCard } from './BoardCard';
import { RowInfo } from '../../stores/effects/database/row/row_cache'; import { RowInfo } from '$app/stores/effects/database/row/row_cache';
import { DatabaseController } from '../../stores/effects/database/database_controller'; import { DatabaseController } from '$app/stores/effects/database/database_controller';
import { Droppable } from 'react-beautiful-dnd'; import { Droppable } from 'react-beautiful-dnd';
import { DatabaseGroupController } from '$app/stores/effects/database/group/group_controller'; import { DatabaseGroupController } from '$app/stores/effects/database/group/group_controller';
export const BoardBlock = ({ export const BoardGroup = ({
viewId, viewId,
controller, controller,
allRows, allRows,

View File

@ -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>
);
};

View File

@ -1,8 +1,8 @@
import { SelectOptionCellDataPB } from '../../../services/backend'; import { SelectOptionCellDataPB } from '@/services/backend';
import { useCell } from '../_shared/database-hooks/useCell'; import { useCell } from '../_shared/database-hooks/useCell';
import { CellIdentifier } from '../../stores/effects/database/cell/cell_bd_svc'; import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
import { CellCache } from '../../stores/effects/database/cell/cell_cache'; import { CellCache } from '$app/stores/effects/database/cell/cell_cache';
import { FieldController } from '../../stores/effects/database/field/field_controller'; import { FieldController } from '$app/stores/effects/database/field/field_controller';
import { getBgColor } from '$app/components/_shared/getColor'; import { getBgColor } from '$app/components/_shared/getColor';
export const BoardOptionsCell = ({ export const BoardOptionsCell = ({
@ -18,7 +18,7 @@ export const BoardOptionsCell = ({
return ( return (
<div className={'flex flex-wrap items-center gap-2 py-2 text-xs text-black'}> <div className={'flex flex-wrap items-center gap-2 py-2 text-xs text-black'}>
{(data as SelectOptionCellDataPB | undefined)?.select_options?.map((option, index) => ( {(data as SelectOptionCellDataPB)?.select_options?.map((option, index) => (
<div className={`${getBgColor(option.color)} rounded px-2 py-0.5`} key={index}> <div className={`${getBgColor(option.color)} rounded px-2 py-0.5`} key={index}>
{option?.name || ''} {option?.name || ''}
</div> </div>

View File

@ -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>
);
};

View File

@ -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,
};
};

View File

@ -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>
);
};

View File

@ -17,12 +17,8 @@ export const BoardUrlCell = ({
return ( return (
<> <>
<a <a className={'text-main-accent hover:underline'} href={(data as URLCellDataPB)?.url || ''} target={'_blank'}>
className={'text-main-accent hover:underline'} {(data as URLCellDataPB)?.content || ''}
href={(data as URLCellDataPB | undefined)?.url || ''}
target={'_blank'}
>
{(data as URLCellDataPB | undefined)?.content || ''}
</a> </a>
</> </>
); );

View File

@ -1,4 +1,4 @@
import { BlockType } from '@/appflowy_app/interfaces/document'; import { BlockType, HeadingBlockData } from '@/appflowy_app/interfaces/document';
import { useAppSelector } from '@/appflowy_app/stores/store'; import { useAppSelector } from '@/appflowy_app/stores/store';
import { debounce } from '@/appflowy_app/utils/tool'; import { debounce } from '@/appflowy_app/utils/tool';
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
@ -43,9 +43,10 @@ export function useBlockSideTools({ container }: { container: HTMLDivElement })
el.style.zIndex = '1'; el.style.zIndex = '1';
el.style.top = '1px'; el.style.top = '1px';
if (node?.type === BlockType.HeadingBlock) { if (node?.type === BlockType.HeadingBlock) {
if (node.data.style?.level === 1) { const nodeData = node.data as HeadingBlockData;
if (nodeData.level === 1) {
el.style.top = '8px'; el.style.top = '8px';
} else if (node.data.style?.level === 2) { } else if (nodeData.level === 2) {
el.style.top = '6px'; el.style.top = '6px';
} else { } else {
el.style.top = '5px'; el.style.top = '5px';
@ -80,16 +81,7 @@ function useController() {
const parentId = node.parent; const parentId = node.parent;
if (!parentId || !controller) return; if (!parentId || !controller) return;
controller.transact([ //
() => {
const newNode = {
id: v4(),
delta: [],
type: BlockType.TextBlock,
};
controller.insert(newNode, parentId, node.id);
},
]);
}, []); }, []);
return { return {

View File

@ -3,11 +3,18 @@ import { useDocumentTitle } from './DocumentTitle.hooks';
import TextBlock from '../TextBlock'; import TextBlock from '../TextBlock';
export default function DocumentTitle({ id }: { id: string }) { export default function DocumentTitle({ id }: { id: string }) {
const { node, delta } = useDocumentTitle(id); const { node } = useDocumentTitle(id);
if (!node) return null; if (!node) return null;
return ( return (
<div data-block-id={node.id} className='doc-title relative pt-[50px] text-4xl font-bold'> <div data-block-id={node.id} className='doc-title relative pt-[50px] text-4xl font-bold'>
<TextBlock placeholder='Untitled' childIds={[]} delta={delta || []} node={node} /> <TextBlock placeholder='Untitled' childIds={[]} node={{
...node,
data: {
...node.data,
delta: node.data.delta || [],
}
}} />
</div> </div>
); );
} }

View File

@ -11,7 +11,7 @@ const fontSize: Record<string, string> = {
export default function HeadingBlock({ node, delta }: { node: Node; delta: TextDelta[] }) { export default function HeadingBlock({ node, delta }: { node: Node; delta: TextDelta[] }) {
return ( return (
<div className={`${fontSize[node.data.style?.level]} font-semibold `}> <div className={`${fontSize[node.data.style?.level]} font-semibold `}>
<TextBlock node={node} childIds={[]} delta={delta} /> {/*<TextBlock node={node} childIds={[]} delta={delta} />*/}
</div> </div>
); );
} }

View File

@ -11,7 +11,7 @@ export default function ListBlock({ node, delta }: { node: Node; delta: TextDelt
if (node.data.style?.type === 'column') return <></>; if (node.data.style?.type === 'column') return <></>;
return ( return (
<div className='flex-1'> <div className='flex-1'>
<TextBlock delta={delta} node={node} childIds={[]} /> {/*<TextBlock delta={delta} node={node} childIds={[]} />*/}
</div> </div>
); );
}, [node, delta]); }, [node, delta]);

View File

@ -6,7 +6,7 @@ const defaultSize = 60;
export function useVirtualizedList(count: number) { export function useVirtualizedList(count: number) {
const parentRef = useRef<HTMLDivElement>(null); const parentRef = useRef<HTMLDivElement>(null);
const Virtualize = useVirtualizer({ const virtualize = useVirtualizer({
count, count,
getScrollElement: () => parentRef.current, getScrollElement: () => parentRef.current,
estimateSize: () => { estimateSize: () => {
@ -15,7 +15,7 @@ export function useVirtualizedList(count: number) {
}); });
return { return {
Virtualize: Virtualize, virtualize,
parentRef, parentRef,
}; };
} }

View File

@ -13,8 +13,8 @@ export default function VirtualizedList({
node: Node; node: Node;
renderNode: (nodeId: string) => JSX.Element; renderNode: (nodeId: string) => JSX.Element;
}) { }) {
const { Virtualize, parentRef } = useVirtualizedList(childIds.length); const { virtualize, parentRef } = useVirtualizedList(childIds.length);
const virtualItems = Virtualize.getVirtualItems(); const virtualItems = virtualize.getVirtualItems();
return ( return (
<> <>
@ -25,7 +25,7 @@ export default function VirtualizedList({
<div <div
className='doc-body max-w-screen w-[900px] min-w-0' className='doc-body max-w-screen w-[900px] min-w-0'
style={{ style={{
height: Virtualize.getTotalSize(), height: virtualize.getTotalSize(),
position: 'relative', position: 'relative',
}} }}
> >
@ -42,7 +42,7 @@ export default function VirtualizedList({
{virtualItems.map((virtualRow) => { {virtualItems.map((virtualRow) => {
const id = childIds[virtualRow.index]; const id = childIds[virtualRow.index];
return ( return (
<div className='p-[1px]' key={id} data-index={virtualRow.index} ref={Virtualize.measureElement}> <div className='p-[1px]' key={id} data-index={virtualRow.index} ref={virtualize.measureElement}>
{virtualRow.index === 0 ? <DocumentTitle id={node.id} /> : null} {virtualRow.index === 0 ? <DocumentTitle id={node.id} /> : null}
{renderNode(id)} {renderNode(id)}
</div> </div>

View File

@ -2,7 +2,7 @@ import { useAppDispatch, useAppSelector } from '../../stores/store';
import { errorActions } from '../../stores/reducers/error/slice'; import { errorActions } from '../../stores/reducers/error/slice';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
export const useError = () => { export const useError = (e: Error) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const error = useAppSelector((state) => state.error); const error = useAppSelector((state) => state.error);
const [errorMessage, setErrorMessage] = useState(''); const [errorMessage, setErrorMessage] = useState('');
@ -13,6 +13,12 @@ export const useError = () => {
setErrorMessage(error.message); setErrorMessage(error.message);
}, [error]); }, [error]);
useEffect(() => {
if (e) {
showError(e.message);
}
}, [e]);
const showError = (msg: string) => { const showError = (msg: string) => {
dispatch(errorActions.showError(msg)); dispatch(errorActions.showError(msg));
}; };

View File

@ -1,8 +1,8 @@
import { useError } from './Error.hooks'; import { useError } from './Error.hooks';
import { ErrorModal } from './ErrorModal'; import { ErrorModal } from './ErrorModal';
export const ErrorHandlerPage = () => { export const ErrorHandlerPage = ({ error }: { error: Error }) => {
const { hideError, errorMessage, displayError } = useError(); const { hideError, errorMessage, displayError } = useError(error);
return displayError ? <ErrorModal message={errorMessage} onClose={hideError}></ErrorModal> : <></>; return displayError ? <ErrorModal message={errorMessage} onClose={hideError}></ErrorModal> : <></>;
}; };

View File

@ -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>
)}
</>
)}
</>
);
};

View File

@ -1,4 +1,3 @@
import { Link } from 'react-router-dom';
import AddSvg from '../../_shared/svg/AddSvg'; import AddSvg from '../../_shared/svg/AddSvg';
export const GridAddView = () => { export const GridAddView = () => {

View File

@ -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} />
)}
</>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
)}
</>
);
}

View File

@ -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>
);
}

View File

@ -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>}</>
);
};

View File

@ -1,4 +1,4 @@
import { useAppSelector } from '../../../stores/store'; import { useAppSelector } from '$app/stores/store';
export const useGridTableCount = () => { export const useGridTableCount = () => {
const { grid } = useAppSelector((state) => state); const { grid } = useAppSelector((state) => state);

View File

@ -1,27 +1,25 @@
import { nanoid } from 'nanoid'; import { useAppSelector } from '$app/stores/store';
import { FieldType } from '@/services/backend/models/flowy-database/field_entities'; import { DatabaseController } from '@/appflowy_app/stores/effects/database/database_controller';
import { gridActions } from '../../../stores/reducers/grid/slice'; import { TypeOptionController } from '@/appflowy_app/stores/effects/database/field/type_option/type_option_controller';
import { useAppDispatch, useAppSelector } from '../../../stores/store'; import { None } from 'ts-results';
export const useGridTableHeaderHooks = function () { export const useGridTableHeaderHooks = function (controller: DatabaseController) {
const dispatch = useAppDispatch(); const database = useAppSelector((state) => state.database);
const grid = useAppSelector((state) => state.grid);
const onAddField = () => { const onAddField = async () => {
dispatch( // TODO: move this to database controller hook
gridActions.addField({ const fieldController = new TypeOptionController(controller.viewId, None);
field: { await fieldController.initialize();
fieldId: nanoid(8),
name: 'Name',
fieldOptions: {},
fieldType: FieldType.RichText,
},
})
);
}; };
return { return {
fields: grid.fields, fields: Object.values(database.fields).map((field) => {
return {
fieldId: field.fieldId,
name: field.title,
fieldType: field.fieldType,
};
}),
onAddField, onAddField,
}; };
}; };

View File

@ -1,49 +1,32 @@
import { DatabaseController } from '@/appflowy_app/stores/effects/database/database_controller';
import AddSvg from '../../_shared/svg/AddSvg'; import AddSvg from '../../_shared/svg/AddSvg';
import { useGridTableHeaderHooks } from './GridTableHeader.hooks'; import { useGridTableHeaderHooks } from './GridTableHeader.hooks';
import { TextTypeSvg } from '../../_shared/svg/TextTypeSvg';
import { NumberTypeSvg } from '../../_shared/svg/NumberTypeSvg';
import { DateTypeSvg } from '../../_shared/svg/DateTypeSvg';
import { SingleSelectTypeSvg } from '../../_shared/svg/SingleSelectTypeSvg';
import { MultiSelectTypeSvg } from '../../_shared/svg/MultiSelectTypeSvg';
import { ChecklistTypeSvg } from '../../_shared/svg/ChecklistTypeSvg';
import { UrlTypeSvg } from '../../_shared/svg/UrlTypeSvg';
import { FieldType } from '@/services/backend/models/flowy-database/field_entities';
export const GridTableHeader = () => { import { GridTableHeaderItem } from './GridTableHeaderItem';
const { fields, onAddField } = useGridTableHeaderHooks(); import { useTranslation } from 'react-i18next';
export const GridTableHeader = ({ controller }: { controller: DatabaseController }) => {
const { fields, onAddField } = useGridTableHeaderHooks(controller);
const { t } = useTranslation('');
return ( return (
<> <>
<thead> <thead>
<tr> <tr>
{fields.map((field, i) => { {fields.map((field, i) => {
return ( return <GridTableHeaderItem field={field} controller={controller} key={i} />;
<th key={field.fieldId} className='m-0 border border-l-0 border-shade-6 p-0'>
<div className={'flex cursor-pointer items-center p-2 hover:bg-main-secondary'}>
<i className={'mr-2 h-5 w-5 text-shade-3'}>
{field.fieldType === FieldType.RichText && <TextTypeSvg></TextTypeSvg>}
{field.fieldType === FieldType.Number && <NumberTypeSvg></NumberTypeSvg>}
{field.fieldType === FieldType.DateTime && <DateTypeSvg></DateTypeSvg>}
{field.fieldType === FieldType.SingleSelect && <SingleSelectTypeSvg></SingleSelectTypeSvg>}
{field.fieldType === FieldType.MultiSelect && <MultiSelectTypeSvg></MultiSelectTypeSvg>}
{field.fieldType === FieldType.Checklist && <ChecklistTypeSvg></ChecklistTypeSvg>}
{field.fieldType === FieldType.URL && <UrlTypeSvg></UrlTypeSvg>}
</i>
<span>{field.name}</span>
</div>
</th>
);
})} })}
<th className='m-0 w-40 border border-r-0 border-shade-6 p-0'> <th className='m-0 w-40 border border-r-0 border-shade-6 p-0'>
<div <div
className='flex cursor-pointer items-center p-2 text-shade-3 hover:bg-main-secondary hover:text-black' className='flex cursor-pointer items-center px-4 py-2 text-shade-3 hover:bg-main-secondary hover:text-black'
onClick={onAddField} onClick={onAddField}
> >
<i className='mr-2 h-5 w-5'> <i className='mr-2 h-5 w-5'>
<AddSvg /> <AddSvg />
</i> </i>
<span>New column</span> <span>{t('grid.field.newColumn')}</span>
</div> </div>
</th> </th>
</tr> </tr>

View File

@ -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>
);
};

View File

@ -1,11 +1,8 @@
import { gridActions } from '../../../stores/reducers/grid/slice'; import { DatabaseController } from '@/appflowy_app/stores/effects/database/database_controller';
import { useAppDispatch } from '../../../stores/store';
export const useGridAddRow = () => { export const useGridAddRow = (controller: DatabaseController) => {
const dispatch = useAppDispatch(); async function addRow() {
await controller.createRow();
function addRow() {
dispatch(gridActions.addRow());
} }
return { return {

View File

@ -1,7 +1,10 @@
import { DatabaseController } from '@/appflowy_app/stores/effects/database/database_controller';
import AddSvg from '../../_shared/svg/AddSvg'; import AddSvg from '../../_shared/svg/AddSvg';
import { useGridAddRow } from './GridAddRow.hooks'; import { useGridAddRow } from './GridAddRow.hooks';
export const GridAddRow = () => { import { useTranslation } from 'react-i18next';
const { addRow } = useGridAddRow(); export const GridAddRow = ({ controller }: { controller: DatabaseController }) => {
const { addRow } = useGridAddRow(controller);
const { t } = useTranslation('');
return ( return (
<div> <div>
@ -9,7 +12,7 @@ export const GridAddRow = () => {
<i className='mr-2 h-5 w-5'> <i className='mr-2 h-5 w-5'>
<AddSvg /> <AddSvg />
</i> </i>
<span>New row</span> <span>{t('grid.row.newRow')}</span>
</button> </button>
</div> </div>
); );

View File

@ -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} />;
};

View File

@ -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,
};
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -1,9 +0,0 @@
import { useAppSelector } from '../../../stores/store';
export const useGridTableRowsHooks = () => {
const grid = useAppSelector((state) => state.grid);
return {
rows: grid.rows,
};
};

View File

@ -1,24 +1,21 @@
import { GridTableItem } from './GridTableItem'; import { DatabaseController } from '@/appflowy_app/stores/effects/database/database_controller';
import { useGridTableRowsHooks } from './GridTableRows.hooks'; import { RowInfo } from '@/appflowy_app/stores/effects/database/row/row_cache';
import { GridTableRow } from './GridTableRow';
export const GridTableRows = () => { export const GridTableRows = ({
const { rows } = useGridTableRowsHooks(); viewId,
controller,
allRows,
onOpenRow,
}: {
viewId: string;
controller: DatabaseController;
allRows: readonly RowInfo[];
onOpenRow: (rowId: RowInfo) => void;
}) => {
return ( return (
<tbody> <tbody>
{rows.map((row, i) => { {allRows.map((row, i) => {
return ( return <GridTableRow onOpenRow={onOpenRow} row={row} key={i} viewId={viewId} controller={controller} />;
<tr key={row.rowId}>
{row.values.map((value) => {
return (
<td key={value.fieldId} className='m-0 border border-l-0 border-shade-6 p-0'>
<GridTableItem rowItem={value} rowId={row.rowId} />
</td>
);
})}
<td className='m-0 border border-r-0 border-shade-6 p-0'></td>
</tr>
);
})} })}
</tbody> </tbody>
); );

View File

@ -1,7 +1,5 @@
import { useAppDispatch, useAppSelector } from '@/appflowy_app/stores/store';
import { useState } from 'react'; import { useState } from 'react';
import { gridActions } from '../../../stores/reducers/grid/slice';
import { useAppDispatch, useAppSelector } from '../../../stores/store';
export const useGridTitleHooks = function () { export const useGridTitleHooks = function () {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -9,16 +7,12 @@ export const useGridTitleHooks = function () {
const [title, setTitle] = useState(grid.title); const [title, setTitle] = useState(grid.title);
const [changingTitle, setChangingTitle] = useState(false); const [changingTitle, setChangingTitle] = useState(false);
const [showOptions, setShowOptions] = useState(false);
const onTitleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => { const onTitleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
setTitle(event.target.value); setTitle(event.target.value);
}; };
const onTitleBlur = () => {
dispatch(gridActions.updateGridTitle({ title }));
setChangingTitle(false);
};
const onTitleClick = () => { const onTitleClick = () => {
setChangingTitle(true); setChangingTitle(true);
}; };
@ -26,8 +20,9 @@ export const useGridTitleHooks = function () {
return { return {
title, title,
onTitleChange, onTitleChange,
onTitleBlur,
onTitleClick, onTitleClick,
changingTitle, changingTitle,
showOptions,
setShowOptions,
}; };
}; };

View File

@ -1,15 +1,21 @@
import { useGridTitleHooks } from './GridTitle.hooks'; import { useGridTitleHooks } from './GridTitle.hooks';
import { SettingsSvg } from '../../_shared/svg/SettingsSvg'; import { SettingsSvg } from '../../_shared/svg/SettingsSvg';
import { GridTitleOptionsPopup } from './GridTitleOptionsPopup';
export const GridTitle = () => { export const GridTitle = () => {
const { title } = useGridTitleHooks(); const { title, showOptions, setShowOptions } = useGridTitleHooks();
return ( return (
<div className={'flex items-center text-xl font-semibold'}> <div className={'relative flex items-center '}>
<div>{title}</div> <div>{title}</div>
<button className={'ml-2 h-5 w-5'}>
<SettingsSvg></SettingsSvg> <div className='flex '>
</button> <button className={'ml-2 h-5 w-5 '} onClick={() => setShowOptions(!showOptions)}>
<SettingsSvg></SettingsSvg>
</button>
{showOptions && <GridTitleOptionsPopup onClose={() => setShowOptions(!showOptions)} />}
</div>
</div> </div>
); );
}; };

View File

@ -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} />;
};

View File

@ -1,17 +1,11 @@
import { GridAddView } from '../GridAddView/GridAddView'; import { GridAddView } from '../GridAddView/GridAddView';
import { SearchInput } from '../../_shared/SearchInput'; import { SearchInput } from '../../_shared/SearchInput';
import { GridSortButton } from './GridSortButton';
import { GridFieldsButton } from './GridFieldsButton';
import { GridFilterButton } from './GridFilterButton';
export const GridToolbar = () => { export const GridToolbar = () => {
return ( return (
<div className='flex shrink-0 items-center gap-4'> <div className='flex shrink-0 items-center gap-4'>
<SearchInput /> <SearchInput />
<GridAddView /> <GridAddView />
<GridFilterButton></GridFilterButton>
<GridSortButton></GridSortButton>
<GridFieldsButton></GridFieldsButton>
</div> </div>
); );
}; };

View File

@ -1,4 +1,4 @@
import { IPopupItem, Popup } from '../../_shared/Popup'; import { IPopupItem, PopupSelect } from '../../_shared/PopupSelect';
import { LogoutSvg } from '../../_shared/svg/LogoutSvg'; import { LogoutSvg } from '../../_shared/svg/LogoutSvg';
export const OptionsPopup = ({ onSignOutClick, onClose }: { onSignOutClick: () => void; onClose: () => void }) => { export const OptionsPopup = ({ onSignOutClick, onClose }: { onSignOutClick: () => void; onClose: () => void }) => {
@ -14,10 +14,10 @@ export const OptionsPopup = ({ onSignOutClick, onClose }: { onSignOutClick: () =
}, },
]; ];
return ( return (
<Popup <PopupSelect
className={'absolute top-[50px] right-[30px] z-10 whitespace-nowrap'} className={'absolute top-[50px] right-[30px] z-10 whitespace-nowrap'}
items={items} items={items}
onOutsideClick={onClose} onOutsideClick={onClose}
></Popup> ></PopupSelect>
); );
}; };

View File

@ -5,7 +5,7 @@ import { IPage, pagesActions } from '../../../stores/reducers/pages/slice';
import { ViewLayoutPB } from '@/services/backend'; import { ViewLayoutPB } from '@/services/backend';
import { AppBackendService } from '../../../stores/effects/folder/app/app_bd_svc'; import { AppBackendService } from '../../../stores/effects/folder/app/app_bd_svc';
import { WorkspaceBackendService } from '../../../stores/effects/folder/workspace/workspace_bd_svc'; import { WorkspaceBackendService } from '../../../stores/effects/folder/workspace/workspace_bd_svc';
import { useError } from '../../error/Error.hooks';
import { AppObserver } from '../../../stores/effects/folder/app/app_observer'; import { AppObserver } from '../../../stores/effects/folder/app/app_observer';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { INITIAL_FOLDER_HEIGHT, PAGE_ITEM_HEIGHT } from '../../_shared/constants'; import { INITIAL_FOLDER_HEIGHT, PAGE_ITEM_HEIGHT } from '../../_shared/constants';
@ -32,9 +32,6 @@ export const useFolderEvents = (folder: IFolder, pages: IPage[]) => {
const appBackendService = new AppBackendService(folder.id); const appBackendService = new AppBackendService(folder.id);
const workspaceBackendService = new WorkspaceBackendService(workspace.id || ''); const workspaceBackendService = new WorkspaceBackendService(workspace.id || '');
// Error
const error = useError();
useEffect(() => { useEffect(() => {
void appObserver.subscribe({ void appObserver.subscribe({
onAppChanged: (change) => { onAppChanged: (change) => {
@ -85,12 +82,8 @@ export const useFolderEvents = (folder: IFolder, pages: IPage[]) => {
}; };
const changeFolderTitle = async (newTitle: string) => { const changeFolderTitle = async (newTitle: string) => {
try { await appBackendService.update({ name: newTitle });
await appBackendService.update({ name: newTitle }); appDispatch(foldersActions.renameFolder({ id: folder.id, newTitle }));
appDispatch(foldersActions.renameFolder({ id: folder.id, newTitle }));
} catch (e: any) {
error.showError(e?.message);
}
}; };
const closeRenamePopup = () => { const closeRenamePopup = () => {
@ -99,24 +92,16 @@ export const useFolderEvents = (folder: IFolder, pages: IPage[]) => {
const deleteFolder = async () => { const deleteFolder = async () => {
closePopup(); closePopup();
try { await appBackendService.delete();
await appBackendService.delete(); appDispatch(foldersActions.deleteFolder({ id: folder.id }));
appDispatch(foldersActions.deleteFolder({ id: folder.id }));
} catch (e: any) {
error.showError(e?.message);
}
}; };
const duplicateFolder = async () => { const duplicateFolder = async () => {
closePopup(); closePopup();
try { const newApp = await workspaceBackendService.createApp({
const newApp = await workspaceBackendService.createApp({ name: folder.title,
name: folder.title, });
}); appDispatch(foldersActions.addFolder({ id: newApp.id, title: folder.title }));
appDispatch(foldersActions.addFolder({ id: newApp.id, title: folder.title }));
} catch (e: any) {
error.showError(e?.message);
}
}; };
const closePopup = () => { const closePopup = () => {
@ -126,77 +111,65 @@ export const useFolderEvents = (folder: IFolder, pages: IPage[]) => {
const onAddNewDocumentPage = async () => { const onAddNewDocumentPage = async () => {
closePopup(); closePopup();
try { const newView = await appBackendService.createView({
const newView = await appBackendService.createView({ name: 'New Document 1',
name: 'New Document 1', layoutType: ViewLayoutPB.Document,
layoutType: ViewLayoutPB.Document, });
});
appDispatch( appDispatch(
pagesActions.addPage({ pagesActions.addPage({
folderId: folder.id, folderId: folder.id,
pageType: ViewLayoutPB.Document, pageType: ViewLayoutPB.Document,
title: newView.name, title: newView.name,
id: newView.id, id: newView.id,
}) })
); );
setShowPages(true); setShowPages(true);
navigate(`/page/document/${newView.id}`); navigate(`/page/document/${newView.id}`);
} catch (e: any) {
error.showError(e?.message);
}
}; };
const onAddNewBoardPage = async () => { const onAddNewBoardPage = async () => {
closePopup(); closePopup();
try { const newView = await appBackendService.createView({
const newView = await appBackendService.createView({ name: 'New Board 1',
name: 'New Board 1', layoutType: ViewLayoutPB.Board,
layoutType: ViewLayoutPB.Board, });
});
setShowPages(true); setShowPages(true);
appDispatch( appDispatch(
pagesActions.addPage({ pagesActions.addPage({
folderId: folder.id, folderId: folder.id,
pageType: ViewLayoutPB.Board, pageType: ViewLayoutPB.Board,
title: newView.name, title: newView.name,
id: newView.id, id: newView.id,
}) })
); );
navigate(`/page/board/${newView.id}`); navigate(`/page/board/${newView.id}`);
} catch (e: any) {
error.showError(e?.message);
}
}; };
const onAddNewGridPage = async () => { const onAddNewGridPage = async () => {
closePopup(); closePopup();
try { const newView = await appBackendService.createView({
const newView = await appBackendService.createView({ name: 'New Grid 1',
name: 'New Grid 1', layoutType: ViewLayoutPB.Grid,
layoutType: ViewLayoutPB.Grid, });
});
setShowPages(true); setShowPages(true);
appDispatch( appDispatch(
pagesActions.addPage({ pagesActions.addPage({
folderId: folder.id, folderId: folder.id,
pageType: ViewLayoutPB.Grid, pageType: ViewLayoutPB.Grid,
title: newView.name, title: newView.name,
id: newView.id, id: newView.id,
}) })
); );
navigate(`/page/grid/${newView.id}`); navigate(`/page/grid/${newView.id}`);
} catch (e: any) {
error.showError(e?.message);
}
}; };
useEffect(() => { useEffect(() => {

View File

@ -1,4 +1,4 @@
import { IPopupItem, Popup } from '../../_shared/Popup'; import { IPopupItem, PopupSelect } from '../../_shared/PopupSelect';
import { EditSvg } from '../../_shared/svg/EditSvg'; import { EditSvg } from '../../_shared/svg/EditSvg';
import { TrashSvg } from '../../_shared/svg/TrashSvg'; import { TrashSvg } from '../../_shared/svg/TrashSvg';
import { CopySvg } from '../../_shared/svg/CopySvg'; import { CopySvg } from '../../_shared/svg/CopySvg';
@ -47,11 +47,11 @@ export const NavItemOptionsPopup = ({
]; ];
return ( return (
<Popup <PopupSelect
onOutsideClick={() => onClose && onClose()} onOutsideClick={() => onClose && onClose()}
items={items} items={items}
className={`absolute right-0`} className={`absolute right-0`}
style={{ top: `${top}px` }} style={{ top: `${top}px` }}
></Popup> ></PopupSelect>
); );
}; };

View File

@ -117,6 +117,7 @@ export const NavigationPanel = ({
{/*<PluginsButton></PluginsButton>*/} {/*<PluginsButton></PluginsButton>*/}
<DesignSpec></DesignSpec> <DesignSpec></DesignSpec>
<AllIcons></AllIcons>
<TestBackendButton></TestBackendButton> <TestBackendButton></TestBackendButton>
{/*Trash Button*/} {/*Trash Button*/}
@ -158,7 +159,7 @@ export const TestBackendButton = () => {
onClick={() => navigate('/page/api-test')} onClick={() => navigate('/page/api-test')}
className={'flex w-full items-center rounded-lg px-4 py-2 hover:bg-surface-2'} className={'flex w-full items-center rounded-lg px-4 py-2 hover:bg-surface-2'}
> >
APITest API Test
</button> </button>
); );
}; };
@ -171,7 +172,19 @@ export const DesignSpec = () => {
onClick={() => navigate('page/colors')} onClick={() => navigate('page/colors')}
className={'flex w-full items-center rounded-lg px-4 py-2 hover:bg-surface-2'} className={'flex w-full items-center rounded-lg px-4 py-2 hover:bg-surface-2'}
> >
Design Specs Color Palette
</button>
);
};
export const AllIcons = () => {
const navigate = useNavigate();
return (
<button
onClick={() => navigate('page/all-icons')}
className={'flex w-full items-center rounded-lg px-4 py-2 hover:bg-surface-2'}
>
All Icons
</button> </button>
); );
}; };

View File

@ -1,23 +1,17 @@
import { useAppDispatch, useAppSelector } from '../../../stores/store'; import { useAppDispatch, useAppSelector } from '../../../stores/store';
import { foldersActions } from '../../../stores/reducers/folders/slice'; import { foldersActions } from '../../../stores/reducers/folders/slice';
import { WorkspaceBackendService } from '../../../stores/effects/folder/workspace/workspace_bd_svc'; import { WorkspaceBackendService } from '../../../stores/effects/folder/workspace/workspace_bd_svc';
import { useError } from '../../error/Error.hooks';
export const useNewFolder = () => { export const useNewFolder = () => {
const appDispatch = useAppDispatch(); const appDispatch = useAppDispatch();
const workspace = useAppSelector((state) => state.workspace); const workspace = useAppSelector((state) => state.workspace);
const workspaceBackendService = new WorkspaceBackendService(workspace.id || ''); const workspaceBackendService = new WorkspaceBackendService(workspace.id || '');
const error = useError();
const onNewFolder = async () => { const onNewFolder = async () => {
try { const newApp = await workspaceBackendService.createApp({
const newApp = await workspaceBackendService.createApp({ name: 'New Folder 1',
name: 'New Folder 1', });
}); appDispatch(foldersActions.addFolder({ id: newApp.id, title: newApp.name }));
appDispatch(foldersActions.addFolder({ id: newApp.id, title: newApp.name }));
} catch (e: any) {
error.showError(e?.message);
}
}; };
return { return {

View File

@ -1,4 +1,4 @@
import { IPopupItem, Popup } from '../../_shared/Popup'; import { IPopupItem, PopupSelect } from '../../_shared/PopupSelect';
import { DocumentSvg } from '../../_shared/svg/DocumentSvg'; import { DocumentSvg } from '../../_shared/svg/DocumentSvg';
import { BoardSvg } from '../../_shared/svg/BoardSvg'; import { BoardSvg } from '../../_shared/svg/BoardSvg';
import { GridSvg } from '../../_shared/svg/GridSvg'; import { GridSvg } from '../../_shared/svg/GridSvg';
@ -47,11 +47,11 @@ export const NewPagePopup = ({
]; ];
return ( return (
<Popup <PopupSelect
onOutsideClick={() => onClose && onClose()} onOutsideClick={() => onClose && onClose()}
items={items} items={items}
className={'absolute right-0'} className={'absolute right-0'}
style={{ top: `${top}px` }} style={{ top: `${top}px` }}
></Popup> ></PopupSelect>
); );
}; };

View File

@ -13,7 +13,6 @@ export const usePageEvents = (page: IPage) => {
const [activePageId, setActivePageId] = useState<string>(''); const [activePageId, setActivePageId] = useState<string>('');
const currentLocation = useLocation(); const currentLocation = useLocation();
const viewBackendService: ViewBackendService = new ViewBackendService(page.id); const viewBackendService: ViewBackendService = new ViewBackendService(page.id);
const error = useError();
useEffect(() => { useEffect(() => {
const { pathname } = currentLocation; const { pathname } = currentLocation;
@ -32,33 +31,21 @@ export const usePageEvents = (page: IPage) => {
}; };
const changePageTitle = async (newTitle: string) => { const changePageTitle = async (newTitle: string) => {
try { await viewBackendService.update({ name: newTitle });
await viewBackendService.update({ name: newTitle }); appDispatch(pagesActions.renamePage({ id: page.id, newTitle }));
appDispatch(pagesActions.renamePage({ id: page.id, newTitle }));
} catch (e: any) {
error.showError(e?.message);
}
}; };
const deletePage = async () => { const deletePage = async () => {
closePopup(); closePopup();
try { await viewBackendService.delete();
await viewBackendService.delete(); appDispatch(pagesActions.deletePage({ id: page.id }));
appDispatch(pagesActions.deletePage({ id: page.id }));
} catch (e: any) {
error.showError(e?.message);
}
}; };
const duplicatePage = () => { const duplicatePage = () => {
closePopup(); closePopup();
try { appDispatch(
appDispatch( pagesActions.addPage({ id: nanoid(8), pageType: page.pageType, title: page.title, folderId: page.folderId })
pagesActions.addPage({ id: nanoid(8), pageType: page.pageType, title: page.title, folderId: page.folderId }) );
);
} catch (e: any) {
error.showError(e?.message);
}
}; };
const closePopup = () => { const closePopup = () => {

View File

@ -3,13 +3,12 @@ import { useAppDispatch, useAppSelector } from '../../stores/store';
import { pagesActions } from '../../stores/reducers/pages/slice'; import { pagesActions } from '../../stores/reducers/pages/slice';
import { workspaceActions } from '../../stores/reducers/workspace/slice'; import { workspaceActions } from '../../stores/reducers/workspace/slice';
import { UserBackendService } from '../../stores/effects/user/user_bd_svc'; import { UserBackendService } from '../../stores/effects/user/user_bd_svc';
import { useError } from '../error/Error.hooks';
export const useWorkspace = () => { export const useWorkspace = () => {
const currentUser = useAppSelector((state) => state.currentUser); const currentUser = useAppSelector((state) => state.currentUser);
const appDispatch = useAppDispatch(); const appDispatch = useAppDispatch();
const error = useError();
const userBackendService: UserBackendService = new UserBackendService(currentUser.id || 0); const userBackendService: UserBackendService = new UserBackendService(currentUser.id || 0);
const loadWorkspaceItems = async () => { const loadWorkspaceItems = async () => {
@ -31,15 +30,11 @@ export const useWorkspace = () => {
} }
} catch (e1) { } catch (e1) {
// create workspace for first start // create workspace for first start
try { const workspace = await userBackendService.createWorkspace({ name: 'New Workspace', desc: '' });
const workspace = await userBackendService.createWorkspace({ name: 'New Workspace', desc: '' }); appDispatch(workspaceActions.updateWorkspace({ id: workspace.id, name: workspace.name }));
appDispatch(workspaceActions.updateWorkspace({ id: workspace.id, name: workspace.name }));
appDispatch(foldersActions.clearFolders()); appDispatch(foldersActions.clearFolders());
appDispatch(pagesActions.clearPages()); appDispatch(pagesActions.clearPages());
} catch (e2: any) {
error.showError(e2?.message);
}
} }
}; };

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -8,7 +8,7 @@ async function testCreateDocument() {
const document = await svc.open().then((result) => result.unwrap()); const document = await svc.open().then((result) => result.unwrap());
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const content = JSON.parse(document.content); // const content = JSON.parse(document.content);
// The initial document content: // The initial document content:
// { // {
// "document": { // "document": {

View File

@ -12,14 +12,21 @@ export enum BlockType {
TableBlock = 'table', TableBlock = 'table',
ColumnBlock = 'column', ColumnBlock = 'column',
} }
export interface HeadingBlockData {
level: number;
}
export interface TextBlockData {
delta: TextDelta[];
}
export interface PageBlockData extends TextBlockData {}
export interface NestedBlock { export interface NestedBlock {
id: string; id: string;
type: BlockType; type: BlockType;
data: { data: Record<string, any>;
delta?: TextDelta[];
};
externalId: string;
externalType: 'text' | 'array' | 'map';
parent: string | null; parent: string | null;
children: string; children: string;
} }

View File

@ -65,7 +65,7 @@ export class DatabaseController {
this.databaseViewCache.initializeWithRows(database.rows); this.databaseViewCache.initializeWithRows(database.rows);
this._callback?.onViewChanged?.(database); this._callback?.onViewChanged?.(database);
return loadGroupResult; return Ok(database.rows);
} else { } else {
return Err(openDatabaseResult.val); return Err(openDatabaseResult.val);
} }

View File

@ -4,27 +4,45 @@ import {
EditPayloadPB, EditPayloadPB,
FlowyError, FlowyError,
OpenDocumentPayloadPB, OpenDocumentPayloadPB,
DocumentDataPB2,
ViewIdPB, ViewIdPB,
OpenDocumentPayloadPBV2,
ApplyActionPayloadPBV2,
BlockActionTypePB,
BlockActionPB,
CloseDocumentPayloadPBV2,
} from '@/services/backend'; } from '@/services/backend';
import { DocumentEventApplyEdit, DocumentEventGetDocument } from '@/services/backend/events/flowy-document'; import { DocumentEventApplyEdit, DocumentEventGetDocument } from '@/services/backend/events/flowy-document';
import { Result } from 'ts-results'; import { Result } from 'ts-results';
import { FolderEventCloseView } from '@/services/backend/events/flowy-folder2'; import { FolderEventCloseView } from '@/services/backend/events/flowy-folder2';
import {
DocumentEvent2ApplyAction,
DocumentEvent2CloseDocument,
DocumentEvent2OpenDocument,
} from '@/services/backend/events/flowy-document2';
export class DocumentBackendService { export class DocumentBackendService {
constructor(public readonly viewId: string) {} constructor(public readonly viewId: string) {}
open = (): Promise<Result<DocumentDataPB, FlowyError>> => { open = (): Promise<Result<DocumentDataPB2, FlowyError>> => {
const payload = OpenDocumentPayloadPB.fromObject({ document_id: this.viewId, version: DocumentVersionPB.V1 }); const payload = OpenDocumentPayloadPBV2.fromObject({
return DocumentEventGetDocument(payload); document_id: this.viewId,
});
return DocumentEvent2OpenDocument(payload);
}; };
applyEdit = (operations: string) => { applyActions = (actions: [BlockActionPB]): Promise<Result<void, FlowyError>> => {
const payload = EditPayloadPB.fromObject({ doc_id: this.viewId, operations: operations }); const payload = ApplyActionPayloadPBV2.fromObject({
return DocumentEventApplyEdit(payload); document_id: this.viewId,
actions: actions,
});
return DocumentEvent2ApplyAction(payload);
}; };
close = () => { close = (): Promise<Result<void, FlowyError>> => {
const payload = ViewIdPB.fromObject({ value: this.viewId }); const payload = CloseDocumentPayloadPBV2.fromObject({
return FolderEventCloseView(payload); document_id: this.viewId,
});
return DocumentEvent2CloseDocument(payload);
}; };
} }

View File

@ -1,29 +1,53 @@
import { DocumentData, BlockType, TextDelta } from '@/appflowy_app/interfaces/document'; import { DocumentData, BlockType } from '@/appflowy_app/interfaces/document';
import { createContext } from 'react'; import { createContext } from 'react';
import { DocumentBackendService } from './document_bd_svc'; import { DocumentBackendService } from './document_bd_svc';
import { FlowyError } from '@/services/backend';
import { DocumentObserver } from './document_observer';
export const DocumentControllerContext = createContext<DocumentController | null>(null); export const DocumentControllerContext = createContext<DocumentController | null>(null);
export class DocumentController { export class DocumentController {
private readonly backendService: DocumentBackendService; private readonly backendService: DocumentBackendService;
private readonly observer: DocumentObserver;
constructor(public readonly viewId: string) { constructor(public readonly viewId: string) {
this.backendService = new DocumentBackendService(viewId); this.backendService = new DocumentBackendService(viewId);
this.observer = new DocumentObserver(viewId);
} }
open = async (): Promise<DocumentData | null> => { open = async (): Promise<DocumentData | FlowyError> => {
const openDocumentResult = await this.backendService.open(); // example:
if (openDocumentResult.ok) { await this.observer.subscribe({
didReceiveUpdate: () => {
console.log('didReceiveUpdate');
},
});
const document = await this.backendService.open();
if (document.ok) {
console.log(document.val);
const blocks: DocumentData["blocks"] = {};
document.val.blocks.forEach((block) => {
blocks[block.id] = {
id: block.id,
type: block.ty as BlockType,
parent: block.parent_id,
children: block.children_id,
data: JSON.parse(block.data),
};
});
const childrenMap: Record<string, string[]> = {};
document.val.meta.children_map.forEach((child, key) => { childrenMap[key] = child.children; });
return { return {
rootId: '', rootId: document.val.page_id,
blocks: {}, blocks,
meta: { meta: {
childrenMap: {}, childrenMap
}, }
}; }
} else {
return null;
} }
return document.val;
}; };
applyActions = ( applyActions = (

View File

@ -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();
};
}

View File

@ -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);
}
}

View File

@ -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;
};

View File

@ -1,22 +1,24 @@
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Board } from '../components/board/Board'; import { Board } from '../components/board/Board';
import { useAppSelector } from '$app/stores/store';
export const BoardPage = () => { export const BoardPage = () => {
const params = useParams(); const params = useParams();
const [viewId, setViewId] = useState(''); const [viewId, setViewId] = useState('');
const pagesStore = useAppSelector((state) => state.pages);
const [title, setTitle] = useState('');
useEffect(() => { useEffect(() => {
if (params?.id?.length) { if (params?.id?.length) {
setViewId(params.id); setViewId(params.id);
// setDatabaseId('testDb'); setTitle(pagesStore.find((page) => page.id === params.id)?.title || '');
} }
}, [params]); }, [params, pagesStore]);
return ( return (
<div className='flex h-full flex-col gap-8 px-8 pt-8'> <div className='flex h-full flex-col gap-8 px-8 pt-8'>
<h1 className='text-4xl font-bold'>Board: {viewId}</h1> {viewId?.length && <Board viewId={viewId} title={title} />}
{viewId?.length && <Board viewId={viewId} />}
</div> </div>
); );
}; };

View File

@ -23,7 +23,7 @@ export const useDocument = () => {
const res = await c.open(); const res = await c.open();
console.log(res) console.log(res)
if (!res) return; if (!res) return;
setDocumentData(res) // setDocumentData(res)
setDocumentId(params.id) setDocumentId(params.id)
})(); })();

View File

@ -1,9 +0,0 @@
export const useGrid = () => {
const loadGrid = async (id: string) => {
console.log('loading grid');
};
return {
loadGrid,
};
};

View File

@ -1,45 +1,22 @@
import { GridAddView } from '../components/grid/GridAddView/GridAddView';
import { GridTableCount } from '../components/grid/GridTableCount/GridTableCount';
import { GridTableHeader } from '../components/grid/GridTableHeader/GridTableHeader';
import { GridAddRow } from '../components/grid/GridTableRows/GridAddRow';
import { GridTableRows } from '../components/grid/GridTableRows/GridTableRows';
import { GridTitle } from '../components/grid/GridTitle/GridTitle';
import { SearchInput } from '../components/_shared/SearchInput';
import { GridToolbar } from '../components/grid/GridToolbar/GridToolbar';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { useGrid } from './GridPage.hooks';
import { useEffect } from 'react'; import { useEffect, useState } from 'react';
import { Grid } from '../components/grid/Grid/Grid';
export const GridPage = () => { export const GridPage = () => {
const params = useParams(); const params = useParams();
const { loadGrid } = useGrid(); const [viewId, setViewId] = useState('');
useEffect(() => { useEffect(() => {
void (async () => { if (params?.id?.length) {
if (!params?.id) return; setViewId(params.id);
await loadGrid(params.id); // setDatabaseId('testDb');
})(); }
}, [params]); }, [params]);
return ( return (
<div className='mx-auto mt-8 flex flex-col gap-8 px-8'> <div className='flex h-full flex-col gap-8 px-8 pt-8'>
<h1 className='text-4xl font-bold'>Grid</h1> <h1 className='text-4xl font-bold'>Grid: {viewId}</h1>
{viewId?.length && <Grid viewId={viewId} />}
<div className='flex w-full items-center justify-between'>
<GridTitle />
<GridToolbar />
</div>
{/* table component view with text area for td */}
<div className='flex flex-col gap-4'>
<table className='w-full table-fixed text-sm'>
<GridTableHeader />
<GridTableRows />
</table>
<GridAddRow />
</div>
<GridTableCount />
</div> </div>
); );
}; };

View File

@ -2,5 +2,6 @@ export * from "./models/flowy-user";
export * from "./models/flowy-document"; export * from "./models/flowy-document";
export * from "./models/flowy-database"; export * from "./models/flowy-database";
export * from "./models/flowy-folder2"; export * from "./models/flowy-folder2";
export * from "./models/flowy-document2";
export * from "./models/flowy-net"; export * from "./models/flowy-net";
export * from "./models/flowy-error"; export * from "./models/flowy-error";

Some files were not shown because too many files have changed in this diff Show More