Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a96ee8b
Dialog queue!
tjlav5 Mar 6, 2020
2c2ca14
Merge branch 'master' into firestore-clear-all
tjlav5 Mar 6, 2020
a4a087a
add tests
tjlav5 Mar 6, 2020
cd5ee37
add test to assert dialogs open
tjlav5 Mar 6, 2020
e0c7b6b
very basic field-preview w/ delete
tjlav5 Mar 6, 2020
4982977
remove accidental file
tjlav5 Mar 6, 2020
fb2f1d3
Merge branch 'master' into firestore-document-fields
tjlav5 Mar 6, 2020
1a58f25
shared state/reducer/dispatch within DocumentContext
tjlav5 Mar 12, 2020
fae560d
switch to reducer on serialize snapshot
tjlav5 Mar 13, 2020
7a40c3d
arrays are handled by lodash getter
tjlav5 Mar 13, 2020
54133d1
Merge branch 'master' into firestore-document-fields
tjlav5 Mar 13, 2020
ce1d0f4
editor uses the same context and reducer
tjlav5 Mar 13, 2020
781fff4
rough add to array/map
tjlav5 Mar 13, 2020
1dce0a9
better type narrowing
tjlav5 Mar 13, 2020
6433a43
type document actions
tjlav5 Mar 13, 2020
2192ab9
clean up comments
tjlav5 Mar 13, 2020
dc62e36
add tests
tjlav5 Mar 13, 2020
a676e8c
Merge branch 'master' into firestore-document-fields
tjlav5 Mar 13, 2020
3ee122d
basic tests
tjlav5 Mar 13, 2020
5e4fab7
PoC reducer with DocumentReference, specifically partial updates/dele…
tjlav5 Mar 15, 2020
e92c879
delineate DocumentPreview from DocumentEditor
tjlav5 Mar 16, 2020
5be3408
address comments
tjlav5 Mar 16, 2020
a94144d
revert dialogqueue
tjlav5 Mar 16, 2020
e500f42
add tests
tjlav5 Mar 17, 2020
c6411b2
switch to fieldpath
tjlav5 Mar 17, 2020
9877d76
Merge branch 'master' into firestore-document-fields
tjlav5 Mar 17, 2020
bfd9086
Merge branch 'master' into firestore-document-fields
tjlav5 Mar 17, 2020
78f135c
Merge branch 'firestore-document-fields' of github.com:firebase/fireb…
tjlav5 Mar 17, 2020
c68267d
switch lodash.last
tjlav5 Mar 17, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 45 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
"express": "^4.17.1",
"firebase": "^7.6.0",
"font-awesome": "^4.7.0",
"immer": "^6.0.1",
"keycode": "^2.2.0",
"lodash.get": "^4.4.2",
"lodash.last": "^3.0.0",
"node-fetch": "^2.6.0",
"react": "^16.11.0",
"react-dom": "^16.11.0",
Expand All @@ -25,7 +28,7 @@
"redux-saga": "^1.1.1",
"rmwc": "^5.7.1",
"typesafe-actions": "^5.1.0",
"typescript": "3.6.4"
"typescript": "^3.8.3"
},
"scripts": {
"start": "react-scripts start",
Expand Down Expand Up @@ -56,6 +59,8 @@
"@firebase/auth-interop-types": "^0.1.1",
"@firebase/component": "^0.1.1",
"@testing-library/react": "^9.3.2",
"@types/lodash.get": "^4.4.6",
"@types/lodash.last": "^3.0.6",
"@types/react-router-dom": "^5.1.2",
"husky": "^3.0.9",
"jest-fetch-mock": "^3.0.1",
Expand Down
12 changes: 11 additions & 1 deletion src/components/Firestore/Collection.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/

import React from 'react';
import { render } from '@testing-library/react';
import { act, render } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { useCollection, useDocumentData } from 'react-firebase-hooks/firestore';

Expand Down Expand Up @@ -44,6 +44,16 @@ it('shows the list of documents in the collection', () => {
</MemoryRouter>
);

act(() =>
collectionReference.setSnapshot({
docs: [
fakeDocumentSnapshot({
ref: fakeDocumentReference({ id: 'cool-doc-1' }),
}),
],
})
);

expect(getByText(/my-stuff/)).not.toBeNull();
expect(queryByText(/Loading documents/)).toBeNull();
expect(getByText(/cool-doc-1/)).not.toBeNull();
Expand Down
6 changes: 2 additions & 4 deletions src/components/Firestore/Document.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import { ApiProvider } from './ApiContext';

import { Document, Root } from './Document';

import { DocumentStateContext } from './Field/DocumentStore';

jest.mock('react-firebase-hooks/firestore');

it('shows the root-id', () => {
Expand All @@ -46,8 +48,6 @@ it('shows the root-id', () => {
});

it('shows the document-id', () => {
useDocumentData.mockReturnValueOnce([]);

const { getByText } = render(
<MemoryRouter>
<ApiProvider value={fakeFirestoreApi()}>
Expand All @@ -60,8 +60,6 @@ it('shows the document-id', () => {
});

it('shows the root collection-list', () => {
useDocumentData.mockReturnValueOnce([]);

const { getByTestId } = render(
<MemoryRouter>
<ApiProvider value={fakeFirestoreApi()}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,17 @@
* limitations under the License.
*/

import { createContext, useContext } from 'react';
import { firestore } from 'firebase';
import { createAction } from 'typesafe-actions';

const DocumentRefContext = createContext<firestore.DocumentReference | null>(
null
);
import { FirestoreAny, FirestoreMap } from '../models';

export const DocumentRefProvider = DocumentRefContext.Provider;
export const useDocumentRef = () => {
const documentRef = useContext(DocumentRefContext);
if (!documentRef) {
throw new Error('You are missing a <DocumentRefProvider>.');
}
return documentRef;
};
export const reset = createAction('@document/reset')<FirestoreMap>();
export const addField = createAction('@document/add')<{
path: string[];
value: FirestoreAny;
}>();
export const updateField = createAction('@document/update')<{
path: string[];
value: FirestoreAny;
}>();
export const deleteField = createAction('@document/delete')<string[]>();
57 changes: 57 additions & 0 deletions src/components/Firestore/DocumentEditor/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import React from 'react';
import { fireEvent, render } from '@testing-library/react';
import { firestore } from 'firebase';

import DocumentEditor from './index';

it('renders an editable field', () => {
const onChange = jest.fn();
const { getByPlaceholderText } = render(
<DocumentEditor value={{ hello: 'world' }} onChange={onChange} />
);
expect(getByPlaceholderText('Field').value).toBe('hello');
expect(getByPlaceholderText('Type').value).toBe('string');
expect(getByPlaceholderText('Value').value).toBe('"world"');

fireEvent.change(getByPlaceholderText('Value'), {
target: { value: 'new' },
});

expect(getByPlaceholderText('Value').value).toBe('"new"');
expect(onChange).toHaveBeenCalledWith({ hello: 'new' });
});

it('renders an editable field with children', () => {
const onChange = jest.fn();

const { getByDisplayValue } = render(
<DocumentEditor
value={{ hello: { foo: ['bar', { spam: 'eggs' }] } }}
onChange={onChange}
/>
);

fireEvent.change(getByDisplayValue('"eggs"'), {
target: { value: 'new' },
});

expect(onChange).toHaveBeenCalledWith({
hello: { foo: ['bar', { spam: 'new' }] },
});
});
105 changes: 105 additions & 0 deletions src/components/Firestore/DocumentEditor/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import React, { useEffect } from 'react';
import last from 'lodash.last';
import { TextField } from '@rmwc/textfield';

import * as actions from './actions';
import {
useDocumentState,
useDocumentDispatch,
useFieldState,
DocumentProvider,
} from './store';
import { FirestoreMap } from '../models';
import { isMap, isArray, getFieldType } from '../utils';

/** Entry point for a Document/Field editor */
const DocumentEditor: React.FC<{
value: FirestoreMap;
onChange?: (value: FirestoreMap) => void;
}> = ({ value, onChange }) => {
return (
<DocumentProvider value={value}>
<RootField onChange={onChange} />
</DocumentProvider>
);
};

/**
* Special representation of a Document Root, where we don't want to show
* the implicit top-level map.
*/
const RootField: React.FC<{
onChange?: (value: FirestoreMap) => void;
}> = ({ onChange }) => {
const state = useDocumentState();

useEffect(() => {
onChange && onChange(state);
}, [onChange, state]);

return (
<>
{Object.keys(state).map(field => (
<NestedEditor key={field} path={[field]} />
))}
</>
);
};

/**
* Field with call-to-actions for editing as well as rendering applicable child-nodes
*/
const NestedEditor: React.FC<{ path: string[] }> = ({ path }) => {
const state = useFieldState(path);
const dispatch = useDocumentDispatch()!;

let childEditors = null;
if (isMap(state)) {
childEditors = Object.keys(state).map(childLeaf => {
const childPath = [...path, childLeaf];
return <NestedEditor key={childLeaf} path={childPath} />;
});
} else if (isArray(state)) {
childEditors = state.map((value, index) => {
const childPath = [...path, `${index}`];
return <NestedEditor key={index} path={childPath} />;
});
}

function handleEditValue(e: React.FormEvent<HTMLInputElement>) {
dispatch(actions.updateField({ path, value: e.currentTarget.value }));
}

return (
<>
<div style={{ display: 'flex' }}>
<TextField readOnly value={last(path)} placeholder="Field" />
<TextField readOnly value={getFieldType(state)} placeholder="Type" />
<TextField
value={JSON.stringify(state)}
onChange={handleEditValue}
placeholder="Value"
/>
</div>
{childEditors && <div>{childEditors}</div>}
</>
);
};

export default DocumentEditor;
Loading