From 120f7153e03f1c549ecfa031bba667552b27d700 Mon Sep 17 00:00:00 2001 From: Chad Burt Date: Wed, 15 Jan 2025 14:35:27 -0800 Subject: [PATCH] Working About page --- packages/api/generated-schema-clean.gql | 98 ++++++++++++++++ packages/api/generated-schema.gql | 12 ++ packages/api/migrations/committed/000347.sql | 35 ++++++ packages/api/migrations/current.sql | 31 ----- packages/api/schema.sql | 71 ++++++++---- packages/api/src/graphileOptions.ts | 2 + packages/api/src/plugins/aboutPagePlugin.ts | 64 +++++++++++ packages/api/src/prosemirror/config.ts | 7 +- .../client/src/admin/AboutPageSettings.tsx | 106 ++++++++++-------- .../MutationStateCheckmarkIndicator.tsx | 91 +++++++++++++++ packages/client/src/editor/config.ts | 16 +-- .../src/formElements/prosemirror-body.css | 4 + packages/client/src/generated/graphql.ts | 37 +++++- packages/client/src/generated/queries.ts | 37 +++++- packages/client/src/projects/AboutPage.tsx | 36 ++++++ packages/client/src/projects/FullSidebar.tsx | 8 ++ packages/client/src/projects/MiniSidebar.tsx | 55 +++++---- .../src/projects/MiniSidebarButtons.tsx | 7 ++ packages/client/src/projects/ProjectApp.tsx | 5 + .../ProjectAccessControlSettings.graphql | 8 ++ .../src/queries/ProjectMetadata.graphql | 4 + 21 files changed, 606 insertions(+), 128 deletions(-) create mode 100644 packages/api/migrations/committed/000347.sql create mode 100644 packages/api/src/plugins/aboutPagePlugin.ts create mode 100644 packages/client/src/components/MutationStateCheckmarkIndicator.tsx create mode 100644 packages/client/src/projects/AboutPage.tsx diff --git a/packages/api/generated-schema-clean.gql b/packages/api/generated-schema-clean.gql index 53fe18684..5a5bc464e 100644 --- a/packages/api/generated-schema-clean.gql +++ b/packages/api/generated-schema-clean.gql @@ -9143,6 +9143,18 @@ type Mutation { """ input: ToggleResponsesPracticeInput! ): ToggleResponsesPracticePayload + updateAboutPageContent( + """ + The exclusive input argument for this mutation. An object type, make sure to see documentation for this object’s fields. + """ + input: UpdateAboutPageContentInput! + ): UpdateAboutPageContentPayload + updateAboutPageEnabled( + """ + The exclusive input argument for this mutation. An object type, make sure to see documentation for this object’s fields. + """ + input: UpdateAboutPageEnabledInput! + ): UpdateAboutPageEnabledPayload """Updates a single `Acl` using a unique key and a patch.""" updateAcl( @@ -10353,6 +10365,16 @@ needed to drive the application. """ type Project implements Node { + aboutPageContents: JSON! + aboutPageEnabled: Boolean! + + """ + Metadata will be returned as directly stored in the SeaSketch + database or computed by fetching from a 3rd party service, + depending on the data source type. + """ + aboutPageRenderedContent: [RenderedAboutPageContent] + """ Admins can control whether a project is public, invite-only, or admins-only. """ @@ -12822,6 +12844,11 @@ type RemoveValidChildSketchClassPayload { query: Query } +type RenderedAboutPageContent { + html: String + lang: String +} + enum RenderUnderType { LABELS LAND @@ -15253,6 +15280,77 @@ type UnsplashUser { username: String! } +"""All input for the `updateAboutPageContent` mutation.""" +input UpdateAboutPageContentInput { + """ + An arbitrary string value with no semantic meaning. Will be included in the + payload verbatim. May be used to track mutations by the client. + """ + clientMutationId: String + content: JSON + lang: String + slug: String +} + +"""The output of our `updateAboutPageContent` mutation.""" +type UpdateAboutPageContentPayload { + """ + The exact same `clientMutationId` that was provided in the mutation input, + unchanged and unused. May be used by a client to track mutations. + """ + clientMutationId: String + + """Reads a single `DataSourcesBucket` that is related to this `Project`.""" + dataSourcesBucket: DataSourcesBucket + project: Project + + """An edge for our `Project`. May be used by Relay 1.""" + projectEdge( + """The method to use when ordering `Project`.""" + orderBy: [ProjectsOrderBy!] = [PRIMARY_KEY_ASC] + ): ProjectsEdge + + """ + Our root query field type. Allows us to run any query from our mutation payload. + """ + query: Query +} + +"""All input for the `updateAboutPageEnabled` mutation.""" +input UpdateAboutPageEnabledInput { + """ + An arbitrary string value with no semantic meaning. Will be included in the + payload verbatim. May be used to track mutations by the client. + """ + clientMutationId: String + enabled: Boolean + slug: String +} + +"""The output of our `updateAboutPageEnabled` mutation.""" +type UpdateAboutPageEnabledPayload { + """ + The exact same `clientMutationId` that was provided in the mutation input, + unchanged and unused. May be used by a client to track mutations. + """ + clientMutationId: String + + """Reads a single `DataSourcesBucket` that is related to this `Project`.""" + dataSourcesBucket: DataSourcesBucket + project: Project + + """An edge for our `Project`. May be used by Relay 1.""" + projectEdge( + """The method to use when ordering `Project`.""" + orderBy: [ProjectsOrderBy!] = [PRIMARY_KEY_ASC] + ): ProjectsEdge + + """ + Our root query field type. Allows us to run any query from our mutation payload. + """ + query: Query +} + """All input for the `updateAclByBasemapId` mutation.""" input UpdateAclByBasemapIdInput { basemapId: Int! diff --git a/packages/api/generated-schema.gql b/packages/api/generated-schema.gql index 3a906898a..5a5bc464e 100644 --- a/packages/api/generated-schema.gql +++ b/packages/api/generated-schema.gql @@ -10368,6 +10368,13 @@ type Project implements Node { aboutPageContents: JSON! aboutPageEnabled: Boolean! + """ + Metadata will be returned as directly stored in the SeaSketch + database or computed by fetching from a 3rd party service, + depending on the data source type. + """ + aboutPageRenderedContent: [RenderedAboutPageContent] + """ Admins can control whether a project is public, invite-only, or admins-only. """ @@ -12837,6 +12844,11 @@ type RemoveValidChildSketchClassPayload { query: Query } +type RenderedAboutPageContent { + html: String + lang: String +} + enum RenderUnderType { LABELS LAND diff --git a/packages/api/migrations/committed/000347.sql b/packages/api/migrations/committed/000347.sql new file mode 100644 index 000000000..dc3e107c6 --- /dev/null +++ b/packages/api/migrations/committed/000347.sql @@ -0,0 +1,35 @@ +--! Previous: sha1:e697656842a7b30ad925e95f11e70f0620739495 +--! Hash: sha1:90ea7a3aac6c1bbbe0a2e80c9e975cf5b1a96421 + +-- Enter migration here +alter table projects add column if not exists about_page_contents jsonb not null default '{}'::jsonb; + +alter table projects add column if not exists about_page_enabled boolean not null default false; + +create or replace function update_about_page_content(slug text, content jsonb, lang text) + returns projects + language sql + security definer + as $$ + update projects + set about_page_contents = jsonb_set(about_page_contents, array[lang], content) + where projects.slug = update_about_page_content.slug + and session_is_admin(projects.id) + returning *; + $$; + +grant execute on function update_about_page_content(text, jsonb, text) to seasketch_user; + +create or replace function update_about_page_enabled(slug text, enabled boolean) + returns projects + security definer + language sql + as $$ + update projects + set about_page_enabled = enabled + where projects.slug = update_about_page_enabled.slug + and session_is_admin(projects.id) + returning *; + $$; + +grant execute on function update_about_page_enabled(text, boolean) to seasketch_user; diff --git a/packages/api/migrations/current.sql b/packages/api/migrations/current.sql index 7b41cf294..8da533983 100644 --- a/packages/api/migrations/current.sql +++ b/packages/api/migrations/current.sql @@ -1,32 +1 @@ -- Enter migration here -alter table projects add column if not exists about_page_contents jsonb not null default '{}'::jsonb; - -alter table projects add column if not exists about_page_enabled boolean not null default false; - -create or replace function update_about_page_content(slug text, content jsonb, lang text) - returns projects - language sql - security definer - as $$ - update projects - set about_page_contents = jsonb_set(about_page_contents, array[lang], content) - where projects.slug = update_about_page_content.slug - and session_is_admin(projects.id) - returning *; - $$; - -grant execute on function update_about_page_content(text, jsonb, text) to seasketch_user; - -create or replace function update_about_page_enabled(slug text, enabled boolean) - returns projects - security definer - language sql - as $$ - update projects - set about_page_enabled = enabled - where projects.slug = update_about_page_enabled.slug - and session_is_admin(projects.id) - returning *; - $$; - -grant execute on function update_about_page_enabled(text, boolean) to seasketch_user; \ No newline at end of file diff --git a/packages/api/schema.sql b/packages/api/schema.sql index b34c52dd4..2824de542 100644 --- a/packages/api/schema.sql +++ b/packages/api/schema.sql @@ -2010,6 +2010,8 @@ CREATE TABLE public.projects ( hide_overlays boolean DEFAULT false NOT NULL, enable_download_by_default boolean DEFAULT false NOT NULL, data_hosting_retention_period interval, + about_page_contents jsonb DEFAULT '{}'::jsonb NOT NULL, + about_page_enabled boolean DEFAULT false NOT NULL, CONSTRAINT disallow_unlisted_public_projects CHECK (((access_control <> 'public'::public.project_access_control_setting) OR (is_listed = true))), CONSTRAINT is_public_key CHECK (((mapbox_public_key IS NULL) OR (mapbox_public_key ~* '^pk\..+'::text))), CONSTRAINT is_secret CHECK (((mapbox_secret_key IS NULL) OR (mapbox_secret_key ~* '^sk\..+'::text))), @@ -6136,22 +6138,6 @@ CREATE FUNCTION public.copy_table_of_contents_item(item_id integer, copy_data_so $$; --- --- Name: copy_table_of_contents_item_recursive(integer, boolean, boolean, integer); Type: FUNCTION; Schema: public; Owner: - --- - -CREATE FUNCTION public.copy_table_of_contents_item_recursive(item_id integer, copy_data_source boolean, append_copy_to_name boolean, project_id integer) RETURNS integer - LANGUAGE plpgsql SECURITY DEFINER - AS $$ - declare - copy_id int; - child record; - begin - copy_id := copy_table_of_contents_item(item_id, copy_data_source, append_copy_to_name, project_id); - end; - $$; - - -- -- Name: copy_table_of_contents_item_recursive(integer, boolean, boolean, integer, public.ltree, text); Type: FUNCTION; Schema: public; Owner: - -- @@ -17554,6 +17540,36 @@ $$; COMMENT ON FUNCTION public.unsubscribed("userId" integer) IS '@omit'; +-- +-- Name: update_about_page_content(text, jsonb, text); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION public.update_about_page_content(slug text, content jsonb, lang text) RETURNS public.projects + LANGUAGE sql SECURITY DEFINER + AS $$ + update projects + set about_page_contents = jsonb_set(about_page_contents, array[lang], content) + where projects.slug = update_about_page_content.slug + and session_is_admin(projects.id) + returning *; + $$; + + +-- +-- Name: update_about_page_enabled(text, boolean); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION public.update_about_page_enabled(slug text, enabled boolean) RETURNS public.projects + LANGUAGE sql SECURITY DEFINER + AS $$ + update projects + set about_page_enabled = enabled + where projects.slug = update_about_page_enabled.slug + and session_is_admin(projects.id) + returning *; + $$; + + -- -- Name: update_basemap_offline_tile_settings(integer, integer, boolean, integer, integer); Type: FUNCTION; Schema: public; Owner: - -- @@ -26453,13 +26469,6 @@ GRANT ALL ON FUNCTION public.copy_sketch_toc_item_recursive_for_forum(parent_id REVOKE ALL ON FUNCTION public.copy_table_of_contents_item(item_id integer, copy_data_source boolean, append_copy_to_name boolean, "projectId" integer, lpath public.ltree, "parentStableId" text) FROM PUBLIC; --- --- Name: FUNCTION copy_table_of_contents_item_recursive(item_id integer, copy_data_source boolean, append_copy_to_name boolean, project_id integer); Type: ACL; Schema: public; Owner: - --- - -REVOKE ALL ON FUNCTION public.copy_table_of_contents_item_recursive(item_id integer, copy_data_source boolean, append_copy_to_name boolean, project_id integer) FROM PUBLIC; - - -- -- Name: FUNCTION copy_table_of_contents_item_recursive(item_id integer, copy_data_source boolean, append_copy_to_name boolean, project_id integer, lpath public.ltree, parent_stable_id text); Type: ACL; Schema: public; Owner: - -- @@ -34419,6 +34428,22 @@ REVOKE ALL ON FUNCTION public.unsubscribed("userId" integer) FROM PUBLIC; GRANT ALL ON FUNCTION public.unsubscribed("userId" integer) TO graphile; +-- +-- Name: FUNCTION update_about_page_content(slug text, content jsonb, lang text); Type: ACL; Schema: public; Owner: - +-- + +REVOKE ALL ON FUNCTION public.update_about_page_content(slug text, content jsonb, lang text) FROM PUBLIC; +GRANT ALL ON FUNCTION public.update_about_page_content(slug text, content jsonb, lang text) TO seasketch_user; + + +-- +-- Name: FUNCTION update_about_page_enabled(slug text, enabled boolean); Type: ACL; Schema: public; Owner: - +-- + +REVOKE ALL ON FUNCTION public.update_about_page_enabled(slug text, enabled boolean) FROM PUBLIC; +GRANT ALL ON FUNCTION public.update_about_page_enabled(slug text, enabled boolean) TO seasketch_user; + + -- -- Name: FUNCTION update_basemap_offline_tile_settings("projectId" integer, "basemapId" integer, use_default boolean, "maxZ" integer, "maxShorelineZ" integer); Type: ACL; Schema: public; Owner: - -- diff --git a/packages/api/src/graphileOptions.ts b/packages/api/src/graphileOptions.ts index 96884a1a2..2652c6e2b 100644 --- a/packages/api/src/graphileOptions.ts +++ b/packages/api/src/graphileOptions.ts @@ -44,6 +44,7 @@ import SearchOverlaysRateLimiterPlugin from "./plugins/searchOverlaysRateLimiter import ProjectBackgroundJobSubscriptionPlugin from "./plugins/projectBackgroundJobSubscriptionPlugin"; import MetadataParserPlugin from "./plugins/metadataParserPlugin"; import ApiKeyPlugin from "./plugins/apiKeyPlugin"; +import AboutPagePlugin from "./plugins/aboutPagePlugin"; const pluginHook = makePluginHook([{ ...PgPubsub, ...SentryPlugin }]); @@ -98,6 +99,7 @@ export default function graphileOptions(): PostGraphileOptions { ProjectBackgroundJobSubscriptionPlugin, MetadataParserPlugin, ApiKeyPlugin, + AboutPagePlugin, // reorderSchemaFields(graphqlSchemaModifiers.fieldOrder), // extraDocumentationPlugin(graphqlSchemaModifiers.documentation), ], diff --git a/packages/api/src/plugins/aboutPagePlugin.ts b/packages/api/src/plugins/aboutPagePlugin.ts new file mode 100644 index 000000000..07c808bcf --- /dev/null +++ b/packages/api/src/plugins/aboutPagePlugin.ts @@ -0,0 +1,64 @@ +import { makeExtendSchemaPlugin, gql } from "graphile-utils"; +import { DOMSerializer, Fragment, Node } from "prosemirror-model"; +import { JSDOM } from "jsdom"; +import { aboutPageSchema } from "../prosemirror/config"; + +const AboutPagePlugin = makeExtendSchemaPlugin((build) => { + return { + typeDefs: gql` + type RenderedAboutPageContent { + lang: String + html: String + } + + extend type Project { + """ + Metadata will be returned as directly stored in the SeaSketch + database or computed by fetching from a 3rd party service, + depending on the data source type. + """ + aboutPageRenderedContent: [RenderedAboutPageContent] + @requires(columns: ["aboutPageContents", "aboutPageEnabled"]) + } + `, + resolvers: { + Project: { + aboutPageRenderedContent: async (project, args, context, info) => { + if (project.aboutPageEnabled && project.aboutPageContents) { + // Assuming project.aboutPageContents is an object with language keys + return Object.entries(project.aboutPageContents).map( + ([lang, content]) => { + try { + const dom = new JSDOM( + `
` + ); + const target = dom.window.document.getElementById("target")!; + const options = { document: dom.window.document }; + let contentNode = Node.fromJSON(aboutPageSchema, content); + DOMSerializer.fromSchema(aboutPageSchema).serializeFragment( + contentNode.content, + options, + target + ); + return { + lang, + html: target.innerHTML, + }; + } catch (e) { + return { + lang, + html: `${(e as Error).message}`, + }; + } + } + ); + } else { + return []; + } + }, + }, + }, + }; +}); + +export default AboutPagePlugin; diff --git a/packages/api/src/prosemirror/config.ts b/packages/api/src/prosemirror/config.ts index 16614264a..9357d7fa5 100644 --- a/packages/api/src/prosemirror/config.ts +++ b/packages/api/src/prosemirror/config.ts @@ -152,4 +152,9 @@ const forums = new Schema({ }), }); -export { forums }; +const aboutPageSchema = new Schema({ + nodes: addListNodes(baseSchema.spec.nodes, "paragraph block*", "block"), + marks: baseMarks, +}); + +export { forums, aboutPageSchema }; diff --git a/packages/client/src/admin/AboutPageSettings.tsx b/packages/client/src/admin/AboutPageSettings.tsx index 75e503c69..67bfe7df2 100644 --- a/packages/client/src/admin/AboutPageSettings.tsx +++ b/packages/client/src/admin/AboutPageSettings.tsx @@ -3,63 +3,44 @@ import { useParams } from "react-router-dom"; import { useProjectMetadataQuery, useUpdateAboutPageContentsMutation, + useUpdateAboutPageEnabledMutation, } from "../generated/graphql"; import { ProseMirror, useProseMirror } from "use-prosemirror"; import { aboutPage as editorConfig } from "../editor/config"; import { useCallback, useEffect, useRef, useState } from "react"; import { EditorView } from "prosemirror-view"; -import { Node, Slice } from "prosemirror-model"; -import { EditorState, Transaction } from "prosemirror-state"; +import { Node } from "prosemirror-model"; +import { EditorState } from "prosemirror-state"; import EditorMenuBar from "../editor/EditorMenuBar"; import languages from "../lang/supported"; -import { useDebouncedFn } from "beautiful-react-hooks"; import useDebounce from "../useDebounce"; +import MutationStateCheckmarkIndicator from "../components/MutationStateCheckmarkIndicator"; +import Switch from "../components/Switch"; const { schema, plugins } = editorConfig; export default function AboutPageSettings() { - const { t, i18n } = useTranslation("admin"); + const { t } = useTranslation("admin"); const { slug } = useParams<{ slug: string }>(); const { data, loading } = useProjectMetadataQuery({ variables: { slug }, }); + const [mutate, mutationState] = useUpdateAboutPageContentsMutation(); + const [enableMutate] = useUpdateAboutPageEnabledMutation(); const [state, setState] = useProseMirror({ schema }); - const [changes, setChanges] = useState(false); const viewRef = useRef<{ view: EditorView }>(); + const [lang, setLang] = useState("EN"); const debouncedDoc = useDebounce(state.doc, 500); - console.log( - loading, - JSON.stringify( - data?.project?.aboutPageContents?.[lang]?.content?.[0]?.content - ) - ); + const [hasChanges, setHasChanges] = useState(false); - // TODO: Problem here with cached projectmetadata setting the contents of the - // editor, and then being updated by the network. stale content shows up after - // refresh. useEffect(() => { const doc = data?.project?.aboutPageContents?.[lang]; - return; - if (state && doc && state.doc) { + if (state && doc && state.doc && !hasChanges) { try { - console.log("dispatching", state.doc); const node = Node.fromJSON(schema, doc); - // const t = state.tr; - // state.tr.replace( - // 0, - // state.doc.content.size, - // new Slice(node.content, 0, 0) - // ); - - // t.doc = node; - // console.log("t", t, node); - // t.replace(0, state.doc.nodeSize, new Slice(node.content, 0, 0)); - // // t.replaceRangeWith(0, state.doc.nodeSize, node); - // console.log(t); - // viewRef.current?.view.dispatch(t); const newState = EditorState.create({ ...state, schema: state.schema, @@ -67,18 +48,20 @@ export default function AboutPageSettings() { doc: node, selection: state.selection, }); - onChange(newState); + setState(newState); } catch (e) { // do nothing - // console.error(e); - throw e; } } - }, [data?.project?.aboutPageContents, lang]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data?.project?.aboutPageContents]); useEffect(() => { if (!loading) { - const startingDocument = data?.project?.aboutPageContents?.[lang]; + let startingDocument: any = data?.project?.aboutPageContents?.["EN"]; + if (lang !== "EN" && lang in data?.project?.aboutPageContents) { + startingDocument = data?.project?.aboutPageContents?.[lang]; + } try { const doc = startingDocument ? Node.fromJSON(schema, startingDocument) @@ -90,6 +73,7 @@ export default function AboutPageSettings() { doc, }); setState(state); + setHasChanges(false); } catch (e) { const doc = startingDocument ? Node.fromJSON(schema, { @@ -120,14 +104,13 @@ export default function AboutPageSettings() { setState(state); } } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [loading, setState, lang]); - const [hasChanges, setHasChanges] = useState(false); - const onChange = useCallback( (state: EditorState) => { - setState(state); setHasChanges(true); + setState(state); }, [setState] ); @@ -141,17 +124,41 @@ export default function AboutPageSettings() { slug, }, }); - setHasChanges(false); } }, [debouncedDoc]); + const enabled = data?.project?.aboutPageEnabled; + return ( <>
-

- {t("About Page")} +

+ {t("About Page")} + +
+ + enableMutate({ + variables: { + enabled: enable, + slug, + }, + }) + } + isToggled={enabled} + /> +

{/* {mutationState.error &&

{mutationState.error.message}

} */}

@@ -163,7 +170,12 @@ export default function AboutPageSettings() { translated content.

-
+
- setLang(e.target.value)} + > {data?.project?.supportedLanguages?.map((l) => { const lang = languages.find((lang) => lang.code === l); @@ -190,7 +205,10 @@ export default function AboutPageSettings() {
{ + if (state === "SAVED" && showSaved) { + const timeout = setTimeout(() => { + setShowSaved(false); + }, 1000); + return () => { + clearTimeout(timeout); + }; + } else if (state !== "SAVED") { + setShowSaved(true); + } + }, [showSaved, state]); + return ( +
+
+ + + + +
+
+ + + +
+ {error && ( +
+ + + +
+ )} +
+ ); +} diff --git a/packages/client/src/editor/config.ts b/packages/client/src/editor/config.ts index 8ffd49d0e..aa89409cf 100644 --- a/packages/client/src/editor/config.ts +++ b/packages/client/src/editor/config.ts @@ -228,6 +228,14 @@ const aboutPageSchema = new Schema({ marks: baseMarks, }); +export const aboutPage = { + schema: aboutPageSchema, + plugins: exampleSetup({ + schema: aboutPageSchema, + menuBar: false, + }), +}; + export const metadata = { schema: metadataSchema, plugins: exampleSetup({ schema: metadataSchema, menuBar: false }), @@ -256,11 +264,3 @@ export const forumPosts = { menuBar: false, }), }; - -export const aboutPage = { - schema: aboutPageSchema, - plugins: exampleSetup({ - schema: aboutPageSchema, - menuBar: false, - }), -}; diff --git a/packages/client/src/formElements/prosemirror-body.css b/packages/client/src/formElements/prosemirror-body.css index 3ebf4a3fa..c74687fab 100644 --- a/packages/client/src/formElements/prosemirror-body.css +++ b/packages/client/src/formElements/prosemirror-body.css @@ -156,3 +156,7 @@ div[attachments="forumAttachments"] { .metadata .ProseMirror td { @apply border p-1; } + +.about-editor .ProseMirror { + height: 100%; +} diff --git a/packages/client/src/generated/graphql.ts b/packages/client/src/generated/graphql.ts index c9fe8f12b..a9ab1c402 100644 --- a/packages/client/src/generated/graphql.ts +++ b/packages/client/src/generated/graphql.ts @@ -9316,6 +9316,12 @@ export type Project = Node & { __typename?: 'Project'; aboutPageContents: Scalars['JSON']; aboutPageEnabled: Scalars['Boolean']; + /** + * Metadata will be returned as directly stored in the SeaSketch + * database or computed by fetching from a 3rd party service, + * depending on the data source type. + */ + aboutPageRenderedContent?: Maybe>>; /** Admins can control whether a project is public, invite-only, or admins-only. */ accessControl: ProjectAccessControlSetting; /** Reads and enables pagination through a set of `User`. */ @@ -11884,6 +11890,12 @@ export enum RenderUnderType { None = 'NONE' } +export type RenderedAboutPageContent = { + __typename?: 'RenderedAboutPageContent'; + html?: Maybe; + lang?: Maybe; +}; + export type RetentionChangeEstimate = { __typename?: 'RetentionChangeEstimate'; bytes?: Maybe; @@ -19218,6 +19230,10 @@ export type UpdateAboutPageContentsMutation = ( & { project?: Maybe<( { __typename?: 'Project' } & Pick + & { aboutPageRenderedContent?: Maybe + )>>> } )> } )> } ); @@ -19235,6 +19251,10 @@ export type UpdateAboutPageEnabledMutation = ( & { project?: Maybe<( { __typename?: 'Project' } & Pick + & { aboutPageRenderedContent?: Maybe + )>>> } )> } )> } ); @@ -19279,7 +19299,10 @@ export type ProjectMetadataFragment = ( & { sketchClasses: Array<( { __typename?: 'SketchClass' } & Pick - )> } + )>, aboutPageRenderedContent?: Maybe + )>>> } ); export type ProjectPublicDetailsMetadataFragment = ( @@ -22693,6 +22716,10 @@ export const ProjectMetadataFragmentDoc = gql` hideOverlays aboutPageContents aboutPageEnabled + aboutPageRenderedContent { + lang + html + } } `; export const ProjectPublicDetailsMetadataFragmentDoc = gql` @@ -29431,6 +29458,10 @@ export const UpdateAboutPageContentsDocument = gql` project { id aboutPageContents + aboutPageRenderedContent { + lang + html + } } } } @@ -29469,6 +29500,10 @@ export const UpdateAboutPageEnabledDocument = gql` project { id aboutPageEnabled + aboutPageRenderedContent { + lang + html + } } } } diff --git a/packages/client/src/generated/queries.ts b/packages/client/src/generated/queries.ts index 6847d371b..37adcafa3 100644 --- a/packages/client/src/generated/queries.ts +++ b/packages/client/src/generated/queries.ts @@ -9314,6 +9314,12 @@ export type Project = Node & { __typename?: 'Project'; aboutPageContents: Scalars['JSON']; aboutPageEnabled: Scalars['Boolean']; + /** + * Metadata will be returned as directly stored in the SeaSketch + * database or computed by fetching from a 3rd party service, + * depending on the data source type. + */ + aboutPageRenderedContent?: Maybe>>; /** Admins can control whether a project is public, invite-only, or admins-only. */ accessControl: ProjectAccessControlSetting; /** Reads and enables pagination through a set of `User`. */ @@ -11882,6 +11888,12 @@ export enum RenderUnderType { None = 'NONE' } +export type RenderedAboutPageContent = { + __typename?: 'RenderedAboutPageContent'; + html?: Maybe; + lang?: Maybe; +}; + export type RetentionChangeEstimate = { __typename?: 'RetentionChangeEstimate'; bytes?: Maybe; @@ -19216,6 +19228,10 @@ export type UpdateAboutPageContentsMutation = ( & { project?: Maybe<( { __typename?: 'Project' } & Pick + & { aboutPageRenderedContent?: Maybe + )>>> } )> } )> } ); @@ -19233,6 +19249,10 @@ export type UpdateAboutPageEnabledMutation = ( & { project?: Maybe<( { __typename?: 'Project' } & Pick + & { aboutPageRenderedContent?: Maybe + )>>> } )> } )> } ); @@ -19277,7 +19297,10 @@ export type ProjectMetadataFragment = ( & { sketchClasses: Array<( { __typename?: 'SketchClass' } & Pick - )> } + )>, aboutPageRenderedContent?: Maybe + )>>> } ); export type ProjectPublicDetailsMetadataFragment = ( @@ -22691,6 +22714,10 @@ export const ProjectMetadataFragmentDoc = /*#__PURE__*/ gql` hideOverlays aboutPageContents aboutPageEnabled + aboutPageRenderedContent { + lang + html + } } `; export const ProjectPublicDetailsMetadataFragmentDoc = /*#__PURE__*/ gql` @@ -25366,6 +25393,10 @@ export const UpdateAboutPageContentsDocument = /*#__PURE__*/ gql` project { id aboutPageContents + aboutPageRenderedContent { + lang + html + } } } } @@ -25376,6 +25407,10 @@ export const UpdateAboutPageEnabledDocument = /*#__PURE__*/ gql` project { id aboutPageEnabled + aboutPageRenderedContent { + lang + html + } } } } diff --git a/packages/client/src/projects/AboutPage.tsx b/packages/client/src/projects/AboutPage.tsx new file mode 100644 index 000000000..3c0b9a627 --- /dev/null +++ b/packages/client/src/projects/AboutPage.tsx @@ -0,0 +1,36 @@ +import { useTranslation } from "react-i18next"; +import { useProjectMetadataQuery } from "../generated/graphql"; +import getSlug from "../getSlug"; + +export default function AboutPage() { + const { data, loading } = useProjectMetadataQuery({ + variables: { slug: getSlug() }, + }); + const { i18n } = useTranslation(); + + const english = data?.project?.aboutPageRenderedContent?.find( + (c) => c && c.lang === "EN" + ); + let content = english?.html || ""; + if (data?.project?.aboutPageRenderedContent) { + const record = data.project.aboutPageRenderedContent.find( + (c) => c && c.lang === i18n.language + ); + if (record?.html) { + content = record.html; + } + } + + if (loading) { + return null; + } else { + return ( +
+
+
+ ); + } +} diff --git a/packages/client/src/projects/FullSidebar.tsx b/packages/client/src/projects/FullSidebar.tsx index 856c8a484..e7d721d5a 100644 --- a/packages/client/src/projects/FullSidebar.tsx +++ b/packages/client/src/projects/FullSidebar.tsx @@ -3,6 +3,7 @@ import { Link, useHistory, useParams } from "react-router-dom"; import { motion } from "framer-motion"; import { useTranslation, Trans } from "react-i18next"; import { + AboutIcon, ForumsIcon, LayerIcon, MapIcon, @@ -163,6 +164,13 @@ export default function FullSidebar({ } /> */} + {data?.project?.aboutPageEnabled && ( + + )} + {data?.project?.aboutPageEnabled && ( + + )} - {data?.project?.hideOverlays !== true && + {data?.project?.hideOverlays !== true && ( } - {data?.project?.hideSketches !== true && } - {data?.project?.hideForums !== true && } + /> + )} + {data?.project?.hideSketches !== true && ( + + )} + {data?.project?.hideForums !== true && ( + + )} {/* ); +export const AboutIcon = ( + +); + +export const AboutButton = curry(AboutIcon); + export const MapButton = curry(MapIcon); export const LayerIcon = ( diff --git a/packages/client/src/projects/ProjectApp.tsx b/packages/client/src/projects/ProjectApp.tsx index 49ec0c2da..7436af534 100644 --- a/packages/client/src/projects/ProjectApp.tsx +++ b/packages/client/src/projects/ProjectApp.tsx @@ -31,6 +31,7 @@ import { MeasureControlContextProvider } from "../MeasureControl"; import ProjectMapLegend from "./ProjectMapLegend"; import { TableOfContentsMetadataModalProvider } from "../dataLayers/TableOfContentsMetadataModal"; import { DataDownloadModalProvider } from "../dataLayers/DataDownloadModal"; +import AboutPage from "./AboutPage"; const LazyOverlays = React.lazy( () => import(/* webpackChunkName: "Overlays" */ "./OverlayLayers") @@ -87,6 +88,7 @@ export default function ProjectApp() { sketches: t("Sketching Tools"), forums: t("Discussion Forums"), settings: t("Cache Settings"), + about: t("About this Project"), }; const { basemaps, tableOfContentsItems, dataLayers, dataSources } = useMapData(mapContext); @@ -182,6 +184,9 @@ export default function ProjectApp() { : "" }`} > + + + diff --git a/packages/client/src/queries/ProjectAccessControlSettings.graphql b/packages/client/src/queries/ProjectAccessControlSettings.graphql index 0725ffec1..42fe37ca9 100644 --- a/packages/client/src/queries/ProjectAccessControlSettings.graphql +++ b/packages/client/src/queries/ProjectAccessControlSettings.graphql @@ -70,6 +70,10 @@ mutation updateAboutPageContents( project { id aboutPageContents + aboutPageRenderedContent { + lang + html + } } } } @@ -79,6 +83,10 @@ mutation updateAboutPageEnabled($slug: String!, $enabled: Boolean!) { project { id aboutPageEnabled + aboutPageRenderedContent { + lang + html + } } } } diff --git a/packages/client/src/queries/ProjectMetadata.graphql b/packages/client/src/queries/ProjectMetadata.graphql index 3871ad6dc..1616314c2 100644 --- a/packages/client/src/queries/ProjectMetadata.graphql +++ b/packages/client/src/queries/ProjectMetadata.graphql @@ -27,6 +27,10 @@ fragment ProjectMetadata on Project { hideOverlays aboutPageContents aboutPageEnabled + aboutPageRenderedContent { + lang + html + } } fragment ProjectPublicDetailsMetadata on PublicProjectDetail {