mirror of
https://github.com/vale981/Vulcan
synced 2025-03-06 10:01:40 -05:00
working on custom collection demo package
This commit is contained in:
parent
ae568422a8
commit
e1744f9f93
28 changed files with 488 additions and 63 deletions
1
packages/custom-collection-demo/client.js
Normal file
1
packages/custom-collection-demo/client.js
Normal file
|
@ -0,0 +1 @@
|
|||
import './lib/modules.js';
|
5
packages/custom-collection-demo/lib/collection.js
Normal file
5
packages/custom-collection-demo/lib/collection.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
const Movies = new Mongo.Collection("movies");
|
||||
|
||||
Movies.typeName = 'Movie';
|
||||
|
||||
export default Movies;
|
51
packages/custom-collection-demo/lib/components/Movie.jsx
Normal file
51
packages/custom-collection-demo/lib/components/Movie.jsx
Normal file
|
@ -0,0 +1,51 @@
|
|||
import Telescope from 'meteor/nova:lib';
|
||||
import React, { PropTypes, Component } from 'react';
|
||||
import NovaForm from "meteor/nova:forms";
|
||||
import { Button } from 'react-bootstrap';
|
||||
import { Accounts } from 'meteor/std:accounts-ui';
|
||||
import { ModalTrigger } from "meteor/nova:core";
|
||||
import Movies from '../collection.js';
|
||||
|
||||
class Movie extends Component {
|
||||
|
||||
renderEdit() {
|
||||
|
||||
const movie = this.props;
|
||||
|
||||
const component = (
|
||||
<ModalTrigger
|
||||
label="Edit Movie"
|
||||
component={<Button bsStyle="primary">Edit Movie</Button>}
|
||||
>
|
||||
<NovaForm
|
||||
collection={Movies}
|
||||
currentUser={this.props.currentUser}
|
||||
document={movie}
|
||||
mutationName="moviesEdit"
|
||||
/>
|
||||
</ModalTrigger>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="item-actions">
|
||||
{this.props.currentUser && this.props.currentUser._id === movie.userId ? component : ""}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
const movie = this.props;
|
||||
|
||||
return (
|
||||
<div key={movie.name} style={{paddingBottom: "15px",marginBottom: "15px", borderBottom: "1px solid #ccc"}}>
|
||||
<h2>{movie.name} ({movie.year})</h2>
|
||||
<p>{movie.review} – by <strong>{movie.user && movie.user.username}</strong></p>
|
||||
{this.renderEdit()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export default Movie;
|
|
@ -0,0 +1,47 @@
|
|||
import Telescope from 'meteor/nova:lib';
|
||||
import React, { PropTypes, Component } from 'react';
|
||||
import NovaForm from "meteor/nova:forms";
|
||||
import { Button } from 'react-bootstrap';
|
||||
import { Accounts } from 'meteor/std:accounts-ui';
|
||||
import { ModalTrigger } from "meteor/nova:core";
|
||||
import withMoviesList from '../containers/withMoviesList';
|
||||
import Movie from './Movie.jsx';
|
||||
import Movies from '../collection.js';
|
||||
|
||||
const LoadMore = props => <a href="#" className="load-more button button--primary" onClick={props.loadMore}>Load More ({props.count}/{props.totalCount})</a>
|
||||
|
||||
class MoviesList extends Component {
|
||||
|
||||
renderNew() {
|
||||
|
||||
const component = (
|
||||
<div className="add-movie">
|
||||
<ModalTrigger
|
||||
title="Add Movie"
|
||||
component={<Button bsStyle="primary">Add Movie</Button>}
|
||||
>
|
||||
<NovaForm
|
||||
collection={Movies}
|
||||
mutationName="moviesNew"
|
||||
currentUser={this.props.currentUser}
|
||||
/>
|
||||
</ModalTrigger>
|
||||
<hr/>
|
||||
</div>
|
||||
)
|
||||
|
||||
return !!this.props.currentUser ? component : "";
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="movies">
|
||||
{this.renderNew()}
|
||||
{this.props.results && this.props.results.map(movie => <Movie key={movie._id} {...movie} currentUser={this.props.currentUser}/>)}
|
||||
{this.props.hasMore ? (this.props.ready ? <LoadMore {...this.props}/> : <p>Loading…</p>) : <p>No more movies</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
export default withMoviesList(MoviesList);
|
|
@ -0,0 +1,30 @@
|
|||
import Telescope from 'meteor/nova:lib';
|
||||
import React, { PropTypes, Component } from 'react';
|
||||
import NovaForm from "meteor/nova:forms";
|
||||
import { Button } from 'react-bootstrap';
|
||||
import { Accounts } from 'meteor/std:accounts-ui';
|
||||
import { ModalTrigger, Messages, FlashContainer } from "meteor/nova:core";
|
||||
import MoviesList from './MoviesList.jsx';
|
||||
|
||||
class MoviesWrapper extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="wrapper">
|
||||
|
||||
{/*<div style={{maxWidth: "300px"}}>
|
||||
<Accounts.ui.LoginForm />
|
||||
</div>
|
||||
|
||||
<FlashContainer component={Telescope.components.FlashMessages}/>
|
||||
*/}
|
||||
|
||||
<div className="main">
|
||||
<MoviesList />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default MoviesWrapper;
|
15
packages/custom-collection-demo/lib/containers/fragments.js
Normal file
15
packages/custom-collection-demo/lib/containers/fragments.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { createFragment } from 'apollo-client';
|
||||
import gql from 'graphql-tag';
|
||||
import Movies from '../collection.js';
|
||||
|
||||
// create fragments used to specify which information to query for
|
||||
const fullMovieInfo = createFragment(gql`
|
||||
fragment fullMovieInfo on Movie {
|
||||
_id
|
||||
name
|
||||
createdAt
|
||||
year
|
||||
}
|
||||
`)
|
||||
|
||||
export { fullMovieInfo };
|
|
@ -0,0 +1,53 @@
|
|||
import Telescope from 'meteor/nova:lib';
|
||||
import React, { PropTypes, Component } from 'react';
|
||||
import Movies from '../collection.js';
|
||||
import { graphql } from 'react-apollo';
|
||||
import gql from 'graphql-tag';
|
||||
import { fullMovieInfo } from './fragments.js';
|
||||
|
||||
export default function withMoviesList (component, options) {
|
||||
return graphql(gql`
|
||||
query getMoviesList($offset: Int, $limit: Int) {
|
||||
movies(offset: $offset, limit: $limit) {
|
||||
...fullMovieInfo
|
||||
}
|
||||
}
|
||||
`, {
|
||||
options(ownProps) {
|
||||
return {
|
||||
variables: {
|
||||
offset: 0,
|
||||
limit: 10
|
||||
},
|
||||
fragments: fullMovieInfo,
|
||||
pollInterval: 20000,
|
||||
};
|
||||
},
|
||||
props(props) {
|
||||
|
||||
const {data: {loading, movies, moviesListTotal, fetchMore}} = props;
|
||||
|
||||
return {
|
||||
loading,
|
||||
results: movies,
|
||||
totalCount: moviesListTotal,
|
||||
count: movies && movies.length,
|
||||
loadMore() {
|
||||
// basically, rerun the query 'getPostsList' with a new offset
|
||||
return fetchMore({
|
||||
variables: { offset: movies.length },
|
||||
updateQuery(previousResults, { fetchMoreResult }) {
|
||||
// no more post to fetch
|
||||
if (!fetchMoreResult.data) {
|
||||
return previousResults;
|
||||
}
|
||||
// return the previous results "augmented" with more
|
||||
return {...previousResults, movies: [...previousResults.movies, ...fetchMoreResult.data.movies]};
|
||||
},
|
||||
});
|
||||
},
|
||||
...props.ownProps // pass on the props down to the wrapped component
|
||||
};
|
||||
},
|
||||
})(component);
|
||||
}
|
6
packages/custom-collection-demo/lib/modules.js
Normal file
6
packages/custom-collection-demo/lib/modules.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
import './collection.js';
|
||||
import './mutations.js';
|
||||
import './permissions.js';
|
||||
import './resolvers.js';
|
||||
import './routes.js';
|
||||
import './schema.js';
|
54
packages/custom-collection-demo/lib/mutations.js
Normal file
54
packages/custom-collection-demo/lib/mutations.js
Normal file
|
@ -0,0 +1,54 @@
|
|||
import Telescope, { newMutation, editMutation, removeMutation } from 'meteor/nova:lib';
|
||||
import Movies from './collection.js'
|
||||
|
||||
// Resolvers
|
||||
Movies.mutations = {
|
||||
|
||||
moviesNew(root, {document}, context) {
|
||||
return newMutation({
|
||||
action: 'movies.new',
|
||||
collection: context.Movies,
|
||||
document: document,
|
||||
currentUser: context.currentUser,
|
||||
validate: true
|
||||
});
|
||||
},
|
||||
|
||||
moviesEdit(root, {documentId, set, unset}, context) {
|
||||
|
||||
const document = context.Movies.findOne(documentId);
|
||||
const action = Users.owns(context.currentUser, document) ? 'posts.edit.own' : 'posts.edit.all';
|
||||
|
||||
return editMutation({
|
||||
action: action,
|
||||
collection: context.Movies,
|
||||
documentId: documentId,
|
||||
set: set,
|
||||
unset: unset,
|
||||
currentUser: context.currentUser,
|
||||
validate: true
|
||||
});
|
||||
},
|
||||
|
||||
moviesRemove(root, {documentId}, context) {
|
||||
|
||||
const document = context.Movies.findOne(documentId);
|
||||
const action = Users.owns(context.currentUser, document) ? 'movies.remove.own' : 'movies.remove.all';
|
||||
|
||||
return removeMutation({
|
||||
action: action,
|
||||
collection: context.Movies,
|
||||
documentId: documentId,
|
||||
currentUser: context.currentUser,
|
||||
validate: true
|
||||
});
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
// GraphQL mutations
|
||||
Telescope.graphQL.addMutation('moviesNew(document: moviesInput) : Movie');
|
||||
Telescope.graphQL.addMutation('moviesEdit(documentId: String, set: moviesInput, unset: moviesUnset) : Movie');
|
||||
Telescope.graphQL.addMutation('moviesRemove(documentId: String) : Movie');
|
||||
|
||||
export default Movies.mutations;
|
14
packages/custom-collection-demo/lib/permissions.js
Normal file
14
packages/custom-collection-demo/lib/permissions.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
import Users from 'meteor/nova:users';
|
||||
|
||||
const defaultActions = [
|
||||
"movies.new",
|
||||
"movies.edit.own",
|
||||
"movies.remove.own",
|
||||
];
|
||||
Users.groups.default.can(defaultActions);
|
||||
|
||||
const adminActions = [
|
||||
"movies.edit.all",
|
||||
"movies.remove.all"
|
||||
];
|
||||
Users.groups.admins.can(adminActions);
|
40
packages/custom-collection-demo/lib/resolvers.js
Normal file
40
packages/custom-collection-demo/lib/resolvers.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
import Telescope from 'meteor/nova:lib';
|
||||
import mutations from './mutations.js';
|
||||
|
||||
const resolvers = {
|
||||
// Movie: {
|
||||
// user(post, args, context) {
|
||||
// return context.Users.findOne({ _id: post.userId }, { fields: context.getViewableFields(context.currentUser, context.Users) });
|
||||
// },
|
||||
// },
|
||||
Query: {
|
||||
movies(root, {offset, limit}, context, info) {
|
||||
const protectedLimit = (limit < 1 || limit > 10) ? 10 : limit;
|
||||
let options = {};
|
||||
options.limit = protectedLimit;
|
||||
options.skip = offset;
|
||||
// keep only fields that should be viewable by current user
|
||||
options.fields = context.getViewableFields(context.currentUser, context.Movies);
|
||||
return context.Movies.find({}, options).fetch();
|
||||
},
|
||||
moviesListTotal(root, args, context) {
|
||||
return context.Movies.find().count();
|
||||
},
|
||||
movie(root, args, context) {
|
||||
return context.Movies.findOne({_id: args._id}, { fields: context.getViewableFields(context.currentUser, context.Movies) });
|
||||
},
|
||||
},
|
||||
Mutation: mutations
|
||||
};
|
||||
|
||||
// add resolvers
|
||||
Telescope.graphQL.addResolvers(resolvers);
|
||||
|
||||
// define GraphQL queries
|
||||
Telescope.graphQL.addQuery(`
|
||||
movies(offset: Int, limit: Int): [Movie]
|
||||
moviesListTotal(foo: Int): Int
|
||||
movie(_id: String): Movie
|
||||
`);
|
||||
|
||||
export default resolvers;
|
5
packages/custom-collection-demo/lib/routes.js
Normal file
5
packages/custom-collection-demo/lib/routes.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import Telescope from 'meteor/nova:lib';
|
||||
import MoviesWrapper from './components/MoviesWrapper.jsx';
|
||||
|
||||
// add new "/movies" route that loads the MoviesWrapper component
|
||||
Telescope.routes.add({ name: "movies", path: "/movies", component: MoviesWrapper });
|
66
packages/custom-collection-demo/lib/schema.js
Normal file
66
packages/custom-collection-demo/lib/schema.js
Normal file
|
@ -0,0 +1,66 @@
|
|||
import Telescope from 'meteor/nova:lib';
|
||||
import Users from 'meteor/nova:users';
|
||||
import Movies from './collection.js';
|
||||
|
||||
const alwaysPublic = user => true;
|
||||
const isLoggedIn = user => !!user;
|
||||
const canEdit = Users.canEdit;
|
||||
|
||||
// define schema
|
||||
const schema = new SimpleSchema({
|
||||
_id: {
|
||||
type: String,
|
||||
optional: true,
|
||||
viewableIf: alwaysPublic
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
control: "text",
|
||||
viewableIf: alwaysPublic,
|
||||
insertableIf: isLoggedIn,
|
||||
editableIf: canEdit
|
||||
},
|
||||
createdAt: {
|
||||
type: Date,
|
||||
viewableIf: alwaysPublic,
|
||||
autoValue: (documentOrModifier) => {
|
||||
if (documentOrModifier && !documentOrModifier.$set) return new Date() // if this is an insert, set createdAt to current timestamp
|
||||
}
|
||||
},
|
||||
year: {
|
||||
type: String,
|
||||
optional: true,
|
||||
control: "text",
|
||||
viewableIf: alwaysPublic,
|
||||
insertableIf: isLoggedIn,
|
||||
editableIf: canEdit
|
||||
},
|
||||
review: {
|
||||
type: String,
|
||||
control: "textarea",
|
||||
viewableIf: alwaysPublic,
|
||||
insertableIf: isLoggedIn,
|
||||
editableIf: canEdit
|
||||
},
|
||||
privateComments: {
|
||||
type: String,
|
||||
optional: true,
|
||||
control: "textarea",
|
||||
viewableIf: alwaysPublic, //fixme
|
||||
insertableIf: isLoggedIn,
|
||||
editableIf: canEdit
|
||||
},
|
||||
userId: {
|
||||
type: String,
|
||||
viewableIf: alwaysPublic,
|
||||
}
|
||||
});
|
||||
|
||||
// attach schema to collection
|
||||
Movies.attachSchema(schema);
|
||||
|
||||
// generate GraphQL schema from SimpleSchema schema
|
||||
Telescope.graphQL.addCollection(Movies);
|
||||
|
||||
// make collection available to resolvers via their context
|
||||
Telescope.graphQL.addToContext({ Movies });
|
45
packages/custom-collection-demo/lib/seed.js
Normal file
45
packages/custom-collection-demo/lib/seed.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
import Movies from './collection.js';
|
||||
import Users from 'meteor/nova:users';
|
||||
import Telescope, { newMutation, editMutation, removeMutation } from 'meteor/nova:lib';
|
||||
|
||||
const seedData = [
|
||||
{
|
||||
name: 'Star Wars',
|
||||
year: '1973',
|
||||
review: `A classic.`,
|
||||
privateComments: `Actually, I don't really like Star Wars…`
|
||||
},
|
||||
{
|
||||
name: 'Die Hard',
|
||||
year: '1987',
|
||||
review: `A must-see if you like action movies.`,
|
||||
privateComments: `I love Bruce Willis so much!`
|
||||
},
|
||||
{
|
||||
name: 'Terminator',
|
||||
year: '1983',
|
||||
review: `Once again, Schwarzenegger shows why he's the boss.`,
|
||||
privateComments: `Terminator is my favorite movie ever. `
|
||||
},
|
||||
{
|
||||
name: 'Jaws',
|
||||
year: '1971',
|
||||
review: 'The original blockbuster.',
|
||||
privateComments: `I'm scared of sharks…`
|
||||
},
|
||||
]
|
||||
|
||||
Meteor.startup(function () {
|
||||
const currentUser = Users.findOne();
|
||||
if (Movies.find().fetch().length === 0) {
|
||||
seedData.forEach(document => {
|
||||
newMutation({
|
||||
action: 'movies.new',
|
||||
collection: Movies,
|
||||
document: document,
|
||||
currentUser: currentUser,
|
||||
validate: false
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
19
packages/custom-collection-demo/package.js
Normal file
19
packages/custom-collection-demo/package.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
Package.describe({
|
||||
name: "custom-collection-demo",
|
||||
summary: "Telescope components package",
|
||||
version: "0.27.4-nova",
|
||||
git: "https://github.com/TelescopeJS/Telescope.git"
|
||||
});
|
||||
|
||||
Package.onUse(function (api) {
|
||||
|
||||
api.versionsFrom(['METEOR@1.0']);
|
||||
|
||||
api.use([
|
||||
'nova:core@0.27.4-nova',
|
||||
]);
|
||||
|
||||
api.mainModule("server.js", "server");
|
||||
api.mainModule("client.js", "client");
|
||||
|
||||
});
|
3
packages/custom-collection-demo/server.js
Normal file
3
packages/custom-collection-demo/server.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import './lib/modules.js';
|
||||
|
||||
import './lib/seed.js';
|
|
@ -35,7 +35,6 @@ Posts.addField(
|
|||
];
|
||||
}
|
||||
},
|
||||
publish: true // make that field public and send it to the client
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
@ -5,15 +5,22 @@ import { makeExecutableSchema } from 'graphql-tools';
|
|||
import { meteorClientConfig, client } from './client.js';
|
||||
|
||||
import { createApolloServer } from './server.js';
|
||||
import typeDefs from './schema';
|
||||
import generateTypeDefs from './schema';
|
||||
|
||||
const schema = makeExecutableSchema({
|
||||
typeDefs,
|
||||
resolvers: Telescope.graphQL.resolvers,
|
||||
});
|
||||
Meteor.startup(function () {
|
||||
|
||||
const typeDefs = generateTypeDefs();
|
||||
|
||||
createApolloServer({
|
||||
schema,
|
||||
Telescope.graphQL.finalSchema = typeDefs;
|
||||
|
||||
const schema = makeExecutableSchema({
|
||||
typeDefs,
|
||||
resolvers: Telescope.graphQL.resolvers,
|
||||
});
|
||||
|
||||
createApolloServer({
|
||||
schema,
|
||||
});
|
||||
});
|
||||
|
||||
export { meteorClientConfig, client };
|
|
@ -1,14 +1,16 @@
|
|||
import Telescope from 'meteor/nova:lib';
|
||||
|
||||
export default schema = [`
|
||||
${Telescope.graphQL.getCollectionsSchemas()}
|
||||
${Telescope.graphQL.getAdditionalSchemas()}
|
||||
const generateTypeDefs = () => [`
|
||||
${Telescope.graphQL.getCollectionsSchemas()}
|
||||
${Telescope.graphQL.getAdditionalSchemas()}
|
||||
|
||||
type Query {
|
||||
${Telescope.graphQL.queries.join('\n')}
|
||||
}
|
||||
type Query {
|
||||
${Telescope.graphQL.queries.join('\n')}
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
${Telescope.graphQL.mutations.join('\n')}
|
||||
}
|
||||
type Mutation {
|
||||
${Telescope.graphQL.mutations.join('\n')}
|
||||
}
|
||||
`];
|
||||
|
||||
export default generateTypeDefs;
|
|
@ -1,31 +0,0 @@
|
|||
Package.describe({
|
||||
name: "nova:demo",
|
||||
summary: "Telescope components package",
|
||||
version: "0.27.4-nova",
|
||||
git: "https://github.com/TelescopeJS/Telescope.git"
|
||||
});
|
||||
|
||||
Package.onUse(function (api) {
|
||||
|
||||
api.versionsFrom(['METEOR@1.0']);
|
||||
|
||||
api.use([
|
||||
|
||||
// Nova packages
|
||||
|
||||
'nova:core@0.27.4-nova',
|
||||
'utilities:react-list-container@0.1.10',
|
||||
|
||||
// third-party packages
|
||||
|
||||
// 'alt:react-accounts-ui@1.1.0'
|
||||
]);
|
||||
|
||||
api.addFiles([
|
||||
'demo-app.jsx'
|
||||
], ['client', 'server']);
|
||||
|
||||
api.export([
|
||||
"Movies"
|
||||
], ['client', 'server'])
|
||||
});
|
|
@ -37,10 +37,12 @@ const newMutation = ({ action, collection, document, currentUser, validate }) =>
|
|||
console.log(collection._name)
|
||||
console.log(document)
|
||||
|
||||
|
||||
const collectionName = collection._name;
|
||||
const schema = collection.simpleSchema()._schema;
|
||||
|
||||
// add userId to document if needed
|
||||
if (!document.userId) document.userId = currentUser._id;
|
||||
|
||||
// if document is not trusted, run validation steps
|
||||
if (validate) {
|
||||
|
||||
|
@ -60,9 +62,6 @@ const newMutation = ({ action, collection, document, currentUser, validate }) =>
|
|||
// validate document against schema
|
||||
collection.simpleSchema().namedContext(`${collectionName}.new`).validate(document);
|
||||
|
||||
// add userId to document
|
||||
document.userId = currentUser._id;
|
||||
|
||||
// run validation callbacks
|
||||
document = Telescope.callbacks.run(`${collectionName}.new.validate`, document, currentUser);
|
||||
}
|
||||
|
|
|
@ -14,6 +14,5 @@ import './methods.js';
|
|||
import './permissions.js';
|
||||
import './resolvers.js';
|
||||
import './mutations.js';
|
||||
import './queries.js';
|
||||
|
||||
export default Posts;
|
|
@ -1,8 +0,0 @@
|
|||
import Telescope from 'meteor/nova:lib';
|
||||
import Posts from './collection.js';
|
||||
|
||||
Telescope.graphQL.addQuery(`
|
||||
posts(terms: Terms, offset: Int, limit: Int): [Post]
|
||||
postsListTotal(terms: Terms): Int
|
||||
post(_id: String): Post
|
||||
`);
|
|
@ -38,3 +38,9 @@ export default resolvers = {
|
|||
};
|
||||
|
||||
Telescope.graphQL.addResolvers(resolvers);
|
||||
|
||||
Telescope.graphQL.addQuery(`
|
||||
posts(terms: Terms, offset: Int, limit: Int): [Post]
|
||||
postsListTotal(terms: Terms): Int
|
||||
post(_id: String): Post
|
||||
`);
|
|
@ -421,5 +421,3 @@ const termsSchema = `
|
|||
Telescope.graphQL.addSchema(termsSchema);
|
||||
|
||||
Telescope.graphQL.addToContext({ Posts });
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue