diff --git a/.cspell.json b/.cspell.json index 4a957e63a3..1b37c48f9a 100644 --- a/.cspell.json +++ b/.cspell.json @@ -23,8 +23,6 @@ "globby", "gsutil", "hakkens", - "howto", - "howtos", "indexeddb", "kamp", "mappins", diff --git a/.github/labels.yml b/.github/labels.yml index 12436ec1a3..94f28c4624 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -18,7 +18,7 @@ { 'name': 'In progress', 'color': '440B89', 'description': '' }, { 'name': 'Mod: DevOps 🤖', 'color': 'BFD4F2', 'description': '' }, { 'name': 'Mod: Discussions 💬', 'color': 'BFD4F2', 'description': '' }, - { 'name': 'Mod: HowTo 📰', 'color': 'BFD4F2', 'description': '' }, + { 'name': 'Mod: Library 📰', 'color': 'BFD4F2', 'description': '' }, { 'name': 'Mod: Maps 🗺', 'color': 'BFD4F2', 'description': '' }, { 'name': 'Mod: Other ⬜️', 'color': 'BFD4F2', 'description': '' }, { 'name': 'Mod: Profiles 👱', 'color': 'BFD4F2', 'description': '' }, diff --git a/BOUNTIES.md b/BOUNTIES.md index b4edd7ae6a..ddbce330f4 100644 --- a/BOUNTIES.md +++ b/BOUNTIES.md @@ -9,7 +9,7 @@ We currently have 3 bounty levels | Level | Amount | It should take... | E.g. | | ----- | ------ | ----------------------------------------------------------- | ----------------------------------- | | 1 | €20 | Roughly 1-2 hours work for a junior/intermediate developer | Small bugfixes, UI tweaks | -| 2 | €40 | Roughly 3-5 hours work for an intermediate/senior developer | Minor feature, e.g. Howto search | +| 2 | €40 | Roughly 3-5 hours work for an intermediate/senior developer | Minor feature, e.g. Library search | | 3 | €85 | Roughly 6+ hours work for a more senior developer | Major feature, e.g. Research module | --- diff --git a/docs/supabase.md b/docs/supabase.md index 89e588a0c7..3b95f1474c 100644 --- a/docs/supabase.md +++ b/docs/supabase.md @@ -45,7 +45,7 @@ How? # Comment Counts -Currently we can sort questions/research/howtos by the number of comments. +Currently we can sort questions/research/library by the number of comments. With supabase there are a few ways we can do this: 1. A comment count view @@ -61,7 +61,7 @@ How? - Whenever a comment is created or deleted, it triggers the update_comment_count function. - The function checks the Operation kind (Insert/Delete), the source_type and source_id. -- Fron the source_type it will update the according content total (howtos, research, questions) that matches the source_id +- Fron the source_type it will update the according content total (library, research, questions) that matches the source_id # Local firebase sync testing/debugging diff --git a/functions/README.md b/functions/README.md index fce1623a1a..0139e5828e 100644 --- a/functions/README.md +++ b/functions/README.md @@ -90,7 +90,7 @@ A couple tips to help implementing: gcloud firestore export gs://[BUCKET_NAME] --collection-ids=[COLLECTION_ID_1],[COLLECTION_ID_2] ``` -E.g. for the staging server, updating howtos: +E.g. for the staging server, updating the library: ``` gcloud firestore export gs://precious-plastics-v4-dev-exports/2020-10-12 --collection-ids=v3_howtos diff --git a/functions/src/Firebase/firestoreDB.ts b/functions/src/Firebase/firestoreDB.ts index f8c5e0d3af..471f95e5e9 100644 --- a/functions/src/Firebase/firestoreDB.ts +++ b/functions/src/Firebase/firestoreDB.ts @@ -1,8 +1,9 @@ -import { firebaseApp } from './admin' +import { getFirestore } from 'firebase-admin/firestore' import { DB_ENDPOINTS } from '../models' -import { getFirestore } from 'firebase-admin/firestore' -import { DBDoc, DBEndpoint } from 'oa-shared/models/db' +import { firebaseApp } from './admin' + +import type { DBDoc, DBEndpoint } from 'oa-shared/models/db' // TODO - ideally should remove default export to force using functions which have mapping export const db = getFirestore(firebaseApp) diff --git a/functions/src/Integrations/firebase-discord.ts b/functions/src/Integrations/firebase-discord.ts index 383eac1985..38ce307406 100644 --- a/functions/src/Integrations/firebase-discord.ts +++ b/functions/src/Integrations/firebase-discord.ts @@ -35,7 +35,7 @@ export const notifyPinPublished = functions .catch(handleErr) }) -export const notifyHowtoPublished = functions +export const notifyLibraryItemPublished = functions .runWith({ memory: '512MB' }) .firestore.document('v3_howtos/{id}') .onUpdate(async (change, context) => { diff --git a/functions/src/Integrations/index.ts b/functions/src/Integrations/index.ts index facc1237fc..a2be19601a 100644 --- a/functions/src/Integrations/index.ts +++ b/functions/src/Integrations/index.ts @@ -1,12 +1,12 @@ -import * as Slack from './firebase-slack' import * as Discord from './firebase-discord' +import * as Slack from './firebase-slack' import * as Patreon from './patreon' exports.notifyPinAwaitingModeration = Slack.notifyPinAwaitingModeration exports.notifyHowtoAwaitingModeration = Slack.notifyHowtoAwaitingModeration exports.notifyPinPublished = Discord.notifyPinPublished -exports.notifyHowtoPublished = Discord.notifyHowtoPublished +exports.notifyLibraryItemPublished = Discord.notifyLibraryItemPublished exports.notifyQuestionPublished = Discord.notifyQuestionPublished exports.notifyResearchUpdatePublished = Discord.notifyResearchUpdatePublished diff --git a/functions/src/aggregations/common.aggregations.ts b/functions/src/aggregations/common.aggregations.ts index a594422726..cbe75e59da 100644 --- a/functions/src/aggregations/common.aggregations.ts +++ b/functions/src/aggregations/common.aggregations.ts @@ -1,11 +1,14 @@ -import { firestore } from 'firebase-admin' -import { Change, logger } from 'firebase-functions' -import { DB_ENDPOINTS } from '../models' -import { db } from '../Firebase/firestoreDB' -import { compareObjectDiffs, splitArrayToChunks } from '../Utils/data.utils' import { FieldValue } from 'firebase-admin/firestore' +import { logger } from 'firebase-functions' import { IModerationStatus } from 'oa-shared/models/moderation' -import { DBEndpoint } from 'oa-shared/models/db' + +import { db } from '../Firebase/firestoreDB' +import { DB_ENDPOINTS } from '../models' +import { compareObjectDiffs, splitArrayToChunks } from '../Utils/data.utils' + +import type { firestore } from 'firebase-admin' +import type { Change } from 'firebase-functions' +import type { DBEndpoint } from 'oa-shared/models/db' type IDocumentRef = FirebaseFirestore.DocumentReference type ICollectionRef = FirebaseFirestore.CollectionReference @@ -130,17 +133,17 @@ export class AggregationHandler { // Seed total useful aggregation if (this.aggregation.targetDocId === 'users_totalUseful') { - const howtos = await db - .collection(DB_ENDPOINTS.howtos) + const library = await db + .collection(DB_ENDPOINTS.library) .where('votedUsefulBy', '!=', []) .where('moderation', '==', IModerationStatus.ACCEPTED) .get() - logger.info(`${targetAggregation} Howtos - ${howtos.docs.length}`) + logger.info(`${targetAggregation} Library - ${library.docs.length}`) const userUseful = {} - if (!howtos.empty) { - for (let i = 0; i < howtos.docs.length; i++) { - const data = howtos.docs[i].data() + if (!library.empty) { + for (let i = 0; i < library.docs.length; i++) { + const data = library.docs[i].data() userUseful[data._createdBy] = (userUseful[data._createdBy] || 0) + data.votedUsefulBy.length } @@ -242,8 +245,8 @@ export class AggregationHandler { private async calculateTotalUseful(id: string) { const userTotalUseful = {} - const howtos = await db - .collection(DB_ENDPOINTS.howtos) + const library = await db + .collection(DB_ENDPOINTS.library) .where('_createdBy', '==', id) .where('votedUsefulBy', '!=', []) .where('moderation', '==', IModerationStatus.ACCEPTED) @@ -251,9 +254,9 @@ export class AggregationHandler { let totalUseful = 0 - if (!howtos.empty) { - for (let i = 0; i < howtos.docs.length; i++) { - const data = howtos.docs[i].data() + if (!library.empty) { + for (let i = 0; i < library.docs.length; i++) { + const data = library.docs[i].data() totalUseful += data.votedUsefulBy.length } } diff --git a/functions/src/database/updates.ts b/functions/src/database/updates.ts index b324bea108..8843b3de91 100644 --- a/functions/src/database/updates.ts +++ b/functions/src/database/updates.ts @@ -28,21 +28,21 @@ export const contentModifiedTimestamp = functions `${logPrefix}Starting contentModifiedTimestamp Setting`, ) - const howtoUpdates = [] + const libraryUpdates = [] const researchUpdates = [] - const howtos = await db.collection(DB_ENDPOINTS.howtos).get() + const library = await db.collection(DB_ENDPOINTS.library).get() const research = await db.collection(DB_ENDPOINTS.research).get() - if (!howtos.empty) { - // Get howto updates - howtos.forEach((ht) => { + if (!library.empty) { + // Get library updates + library.forEach((ht) => { const data = ht.data() - howtoUpdates.push({ + libraryUpdates.push({ id: ht.id, _contentModifiedTimestamp: data._modified, }) }) - await batchGeneration(howtoUpdates, 'howtos', dryRun, logPrefix) + await batchGeneration(libraryUpdates, 'library', dryRun, logPrefix) } if (!research.empty) { @@ -68,8 +68,8 @@ export const contentModifiedTimestamp = functions _dryRun: dryRun, operations, meta: { - howtoUpdates: howtoUpdates, - researchUpdates: researchUpdates, + libraryUpdates, + researchUpdates, }, } } catch (error) { @@ -83,7 +83,7 @@ export const contentModifiedTimestamp = functions async function batchGeneration( updateData: Record[], - collection: 'howtos' | 'research', + collection: 'library' | 'research', dryRun: boolean, logPrefix: string, ) { diff --git a/functions/src/emailNotifications/createModerationEmails.spec.ts b/functions/src/emailNotifications/createModerationEmails.spec.ts index 8ad2685764..10278bb1eb 100644 --- a/functions/src/emailNotifications/createModerationEmails.spec.ts +++ b/functions/src/emailNotifications/createModerationEmails.spec.ts @@ -1,5 +1,14 @@ -import { FirebaseEmulatedTest } from '../test/Firebase/emulator' +import { IModerationStatus } from 'oa-shared' + +import { getMockLibraryItem } from '../emulator/seed/content-generate' import { DB_ENDPOINTS } from '../models' +import { FirebaseEmulatedTest } from '../test/Firebase/emulator' +import { PP_SIGNOFF } from './constants' +import { + createHowtoModerationEmail, + createMapPinModerationEmail, + handleModerationUpdate, +} from './createModerationEmails' import { HOW_TO_APPROVAL_SUBJECT, HOW_TO_NEEDS_IMPROVEMENTS_SUBJECT, @@ -10,15 +19,8 @@ import { MAP_PIN_REJECTED_SUBJECT, MAP_PIN_SUBMISSION_SUBJECT, } from './templateHelpers' -import { getMockHowto } from '../emulator/seed/content-generate' -import { - createHowtoModerationEmail, - createMapPinModerationEmail, - handleModerationUpdate, -} from './createModerationEmails' -import { PP_SIGNOFF } from './constants' -import { IUserDB } from 'oa-shared/models/user' -import { IModerationStatus } from 'oa-shared' + +import type { IUserDB } from 'oa-shared/models/user' jest.mock('../Firebase/auth', () => ({ firebaseAuth: { @@ -43,7 +45,7 @@ const userFactory = (_id: string, user: Partial = {}): IUserDB => ...user, }) as IUserDB -describe('Create howto moderation emails', () => { +describe('Create library moderation emails', () => { const db = FirebaseEmulatedTest.admin.firestore() beforeEach(async () => { @@ -62,17 +64,17 @@ describe('Create howto moderation emails', () => { await FirebaseEmulatedTest.clearFirestoreDB() }) - it('Creates an email for an accepted howto', async () => { - const howtoApproved = getMockHowto('user_1') - const howtoAwaitingModeration = { - ...howtoApproved, + it('Creates an email for an accepted library items', async () => { + const projectApproved = getMockLibraryItem('user_1') + const projectAwaitingModeration = { + ...projectApproved, moderation: IModerationStatus.AWAITING_MODERATION, } const change = FirebaseEmulatedTest.mockFirestoreChangeObject( - howtoAwaitingModeration, - howtoApproved, - 'howtos', - howtoApproved._id, + projectAwaitingModeration, + projectApproved, + 'library', + projectApproved._id, ) await handleModerationUpdate(change, createHowtoModerationEmail) @@ -103,7 +105,10 @@ describe('Create howto moderation emails', () => { }) it('Creates an email for a howto awaiting moderation', async () => { - const howtoRejected = getMockHowto('user_1', IModerationStatus.REJECTED) + const howtoRejected = getMockLibraryItem( + 'user_1', + IModerationStatus.REJECTED, + ) const howtoAwaitingModeration = { ...howtoRejected, moderation: IModerationStatus.AWAITING_MODERATION, @@ -111,7 +116,7 @@ describe('Create howto moderation emails', () => { const change = FirebaseEmulatedTest.mockFirestoreChangeObject( howtoRejected, howtoAwaitingModeration, - 'howtos', + 'library', howtoAwaitingModeration._id, ) @@ -143,7 +148,7 @@ describe('Create howto moderation emails', () => { }) it('Creates an email for a rejected howto', async () => { - const howtoAwaitingModeration = getMockHowto( + const howtoAwaitingModeration = getMockLibraryItem( 'user_1', IModerationStatus.AWAITING_MODERATION, ) @@ -154,7 +159,7 @@ describe('Create howto moderation emails', () => { const change = FirebaseEmulatedTest.mockFirestoreChangeObject( howtoAwaitingModeration, howtoRejected, - 'howtos', + 'library', howtoAwaitingModeration._id, ) @@ -189,7 +194,7 @@ describe('Create howto moderation emails', () => { }) it('Creates an email for a howto that needs improvements', async () => { - const howtoAwaitingModeration = getMockHowto( + const howtoAwaitingModeration = getMockLibraryItem( 'user_1', IModerationStatus.AWAITING_MODERATION, ) @@ -202,7 +207,7 @@ describe('Create howto moderation emails', () => { const change = FirebaseEmulatedTest.mockFirestoreChangeObject( howtoAwaitingModeration, howtoNeedsImprovements, - 'howtos', + 'library', howtoAwaitingModeration._id, ) @@ -238,8 +243,8 @@ describe('Create howto moderation emails', () => { }) }) - it('Does not create an email for non-approved howtos', async () => { - const howtoApproved = getMockHowto('user_1') + it('Does not create an email for non-approved library items', async () => { + const howtoApproved = getMockLibraryItem('user_1') const howtoDraft = { ...howtoApproved, moderation: IModerationStatus.DRAFT, @@ -247,7 +252,7 @@ describe('Create howto moderation emails', () => { const change = FirebaseEmulatedTest.mockFirestoreChangeObject( howtoApproved, howtoDraft, - 'howtos', + 'library', howtoApproved._id, ) diff --git a/functions/src/emailNotifications/createModerationEmails.ts b/functions/src/emailNotifications/createModerationEmails.ts index 8a58943141..96b1c9fbec 100644 --- a/functions/src/emailNotifications/createModerationEmails.ts +++ b/functions/src/emailNotifications/createModerationEmails.ts @@ -82,7 +82,7 @@ export async function createMapPinModerationEmail(mapPin: IMapPin) { export const handleHowToModerationUpdate = functions .runWith({ memory: MEMORY_LIMIT_512_MB }) - .firestore.document(`${DB_ENDPOINTS.howtos}/{id}`) + .firestore.document(`${DB_ENDPOINTS.library}/{id}`) .onUpdate((change, context) => withErrorAlerting(context, handleModerationUpdate, [ change, diff --git a/functions/src/emailNotifications/createSubmissionEmails.ts b/functions/src/emailNotifications/createSubmissionEmails.ts index bd01a04ab3..47ae798126 100644 --- a/functions/src/emailNotifications/createSubmissionEmails.ts +++ b/functions/src/emailNotifications/createSubmissionEmails.ts @@ -68,7 +68,7 @@ export const handleMessageSubmission = functions export const handleHowToSubmission = functions .runWith({ memory: '512MB' }) - .firestore.document(`${DB_ENDPOINTS.howtos}/{id}`) + .firestore.document(`${DB_ENDPOINTS.library}/{id}`) .onCreate((snapshot, context) => withErrorAlerting(context, createHowtoSubmissionEmail, [snapshot.data()]), ) diff --git a/functions/src/emulator/seed/content-generate.ts b/functions/src/emulator/seed/content-generate.ts index 53034e91aa..9aa7dcccfc 100644 --- a/functions/src/emulator/seed/content-generate.ts +++ b/functions/src/emulator/seed/content-generate.ts @@ -7,25 +7,25 @@ import type { ILibrary, IUserDB } from 'oa-shared' import type { IMockAuthUser } from 'oa-shared/mocks/auth' /** - * Populate additional mock howtos alongside production data for ease of testing + * Populate additional mock library projects alongside production data for ease of testing * TODO - should create from factories used in unit mocks once available in shared */ export async function seedContentGenerate() { - // create mock howtos just for demo_beta_tester and demo_admin users + // create mock library projects just for demo_beta_tester and demo_admin users for (const user of Object.values(MOCK_AUTH_USERS).slice(1, 3)) { - await setMockHowto(user) + await setMockLibrary(user) await setMockNotifications(user) } return } -export function getMockHowto( +export function getMockLibraryItem( uid: string, moderation: ILibrary.DB['moderation'] = IModerationStatus.ACCEPTED, ) { const _id = `00_${uid}_howto` const loginInfo = `username : ${uid}@example.com\npassword : ${uid}` - const howto: ILibrary.DB = { + const library: ILibrary.DB = { _id, _created: new Date().toISOString(), _modified: new Date().toISOString(), @@ -47,13 +47,14 @@ export function getMockHowto( previousSlugs: [_id], totalComments: 0, } - return howto + + return library } -export async function setMockHowto(user: Pick) { +export async function setMockLibrary(user: Pick) { const { uid } = user - const howto = getMockHowto(uid) - await setDoc('howtos', howto._id, howto) + const library = getMockLibraryItem(uid) + await setDoc('library', library._id, library) } async function setMockNotifications(user: IMockAuthUser) { diff --git a/functions/src/emulator/seed/data-clean.ts b/functions/src/emulator/seed/data-clean.ts index 80250dc262..367c7d3c5c 100644 --- a/functions/src/emulator/seed/data-clean.ts +++ b/functions/src/emulator/seed/data-clean.ts @@ -1,18 +1,18 @@ // (note - typings don't currently exist for firebase-tools: https://github.com/firebase/firebase-tools/issues/2378) +import axios from 'axios' import * as firebase_tools from 'firebase-tools' -import { DB_ENDPOINTS } from '../../models' + import { db } from '../../Firebase/firestoreDB' +import { DB_ENDPOINTS } from '../../models' import { splitArrayToChunks } from '../../Utils/data.utils' -import type { firestore } from 'firebase-admin' -import axios from 'axios' -const USE_SMALL_SAMPLE_SEED = false +import type { firestore } from 'firebase-admin' /** * Script used to generate a cleaner export of seed data for use in development * * Given a full dump of site data in the emulator, strips away all user docs and revisions - * for users that have not posted content to mappins, howtos or research + * for users that have not posted content to mappins, library or research * (basically anywhere their profile might be linked from) */ export async function seedDataClean() { @@ -36,13 +36,13 @@ export async function seedDataClean() { // setup variables and types for tracking data const keptUsers = {} - const endpointsToCheck = ['mappins', 'howtos', 'research'] as const + const endpointsToCheck = ['mappins', 'library', 'research'] as const type ICheckedEndpoint = (typeof endpointsToCheck)[number] const allDocs: { [endpoint in ICheckedEndpoint]: firestore.QuerySnapshot } = {} as any - // Get list of users with howtos or mappins to retain data + // Get list of users with library or mappins to retain data for (const endpoint of endpointsToCheck) { const mappedEndpoint = DB_ENDPOINTS[endpoint] if (mappedEndpoint) { @@ -88,34 +88,6 @@ export async function seedDataClean() { return returnMessage } -/** - * WiP - If limiting seed size reduce the overall user list - * TODO - this will be too random and also end up with lots more howtos than anything else. Ideally should provide - */ -async function WiPReduceSeedSize(allDocs) { - // specific list of data - if (USE_SMALL_SAMPLE_SEED) { - const sampleSize = 20 - const keptUsers = {} - const shuffledHowtos = allDocs.howtos.docs - .sort(() => 0.5 - Math.random()) - .slice(0, sampleSize) - shuffledHowtos.forEach((doc) => (keptUsers[doc.data()._createdBy] = true)) - // Delete howtos - const deletedHowtos = await batchDeleteDocs( - allDocs.howtos.docs.filter((d) => !keptUsers[d.data()._createdBy]), - '[Howtos Deleted]', - ) - // Delete mappins - const deletedMappins = await batchDeleteDocs( - allDocs.mappins.docs.filter((d) => !keptUsers[d.id]), - '[Mappins Deleted]', - ) - // Delete research (TBD) - return { deletedHowtos, deletedMappins, keptUsers } - } -} - /** Execute a firestore query and delete all retrieved docs, subject to optional filter function */ async function deleteQueryDocs( query: @@ -166,67 +138,3 @@ async function deleteCollectionAPI(endpoint: string) { 'http://0.0.0.0:4003/emulator/v1/projects/demo-community-platform-emulated' // http://[::1] for non-docker env return axios.delete(`${apiHost}/databases/(default)/documents/${endpoint}`) } - -/** - * Use firebase tools to fully delete collection and subcollection from command-line tools - * https://firebase.google.com/docs/firestore/solutions/delete-collections - * - * NOTE CC 2021-09-29 - Seems to have stopped working with current emulators (not sure why) - * reverting to api method instead. Might be fixed with updated host endpoint used above and in firebase.json - */ -async function deleteCollectionCLI(endpoint: string) { - // Note - whilst we are only operating on the emulator user will probably - // still need to be logged into firebase to call - // https://github.com/firebase/firebase-tools/issues/1940 - await firebase_tools.firestore - .delete(endpoint, { - recursive: true, - yes: true, - }) - .catch((err) => { - console.error(err) - process.exit(1) - }) -} - -/** - * Iterative method to delete all documents in a collection, by reading all documents in batches and deleting - * Copied from https://github.com/firebase/snippets-node/blob/e5f6214059bdbc63f94ba6600f7f84e96325548d/firestore/main/index.js#L889-L921 - * - * Note 1 - this is less efficient than the cli method, but also available outside of node environment - * Note 2 - This fails to delete subcollections, and manual methods above added instead - */ -async function deleteCollectionIteratively( - collectionPath: string, - batchSize = 500, -) { - const collectionRef = db.collection(collectionPath) - const query = collectionRef.orderBy('__name__').limit(batchSize) - return new Promise((resolve, reject) => { - deleteQueryBatch(query, resolve).catch(reject) - }) -} - -async function deleteQueryBatch(query, resolve) { - const snapshot = await query.get() - - const batchSize = snapshot.size - if (batchSize === 0) { - // When there are no documents left, we are done - resolve() - return - } - - // Delete documents in a batch - const batch = db.batch() - snapshot.docs.forEach((doc) => { - batch.delete(doc.ref) - }) - await batch.commit() - - // Recurse on the next process tick, to avoid - // exploding the stack. - process.nextTick(async () => { - await deleteQueryBatch(query, resolve) - }) -} diff --git a/functions/src/models.ts b/functions/src/models.ts index 4b277b1a0c..931910c8d2 100644 --- a/functions/src/models.ts +++ b/functions/src/models.ts @@ -1,4 +1,3 @@ -import type * as functions from 'firebase-functions' // Import and Re-export models from the main platform if required by functions // NOTE - as of yarn 3.3 we need to import relatively instead of from a workspace import. // This is because the functions code sits at same level of main platform package.json, creating cyclic symlinks @@ -6,12 +5,13 @@ import type * as functions from 'firebase-functions' // Importing from outside the src code is still fine because we make single builds with webpack // which can resolve at build time, but would not work if deploying direct to firebase functions. // Alternative fix would be to put the platform code one level further nested e.g. /platform/src - import { dbEndpointSubcollections, generateDBEndpoints, } from 'oa-shared/models/db' +import type * as functions from 'firebase-functions' + export const DB_ENDPOINTS = generateDBEndpoints() export const DB_ENDPOINT_SUBCOLLECTIONS = dbEndpointSubcollections diff --git a/functions/src/userUpdates/index.test.ts b/functions/src/userUpdates/index.test.ts index 749761493e..73c3796f2e 100644 --- a/functions/src/userUpdates/index.test.ts +++ b/functions/src/userUpdates/index.test.ts @@ -25,13 +25,13 @@ describe('userUpdates', () => { } describe('updateDocuments', () => { - it('updates howto when user country has changed', async () => { + it('updates library project when user country has changed', async () => { // Arrange const userId = uuid() const newCountryCode = 'nl' const wrapped = test.wrap(handleUserUpdates) - await db.collection(DB_ENDPOINTS.howtos).add({ + await db.collection(DB_ENDPOINTS.library).add({ _createdBy: userId, creatorCountry: 'uk', }) @@ -57,7 +57,7 @@ describe('userUpdates', () => { // Assert const doc = ( await db - .collection(DB_ENDPOINTS.howtos) + .collection(DB_ENDPOINTS.library) .where('_createdBy', '==', userId) .get() ).docs[0].data() @@ -65,13 +65,13 @@ describe('userUpdates', () => { expect(doc.creatorCountry).toBe(newCountryCode) }) - it.skip('updates howto comments when user country has changed', async () => { + it.skip('updates library comments when user country has changed', async () => { // Arrange const userId = uuid() const newCountryCode = 'nl' const wrapped = test.wrap(handleUserUpdates) - await db.collection(DB_ENDPOINTS.howtos).add({ + await db.collection(DB_ENDPOINTS.library).add({ _createdBy: userId, creatorCountry: 'uk', comments: [ @@ -102,7 +102,7 @@ describe('userUpdates', () => { // Assert const doc = ( await db - .collection(DB_ENDPOINTS.howtos) + .collection(DB_ENDPOINTS.library) .where('comments', 'array-contains', { _creatorId: userId, creatorCountry: newCountryCode, @@ -113,13 +113,13 @@ describe('userUpdates', () => { expect(doc?.comments[0].creatorCountry).toBe(newCountryCode) }) - it.skip('updates the comments on multiple howtos when a user country has changed', async () => { + it.skip('updates the comments on library when a user country has changed', async () => { // Arrange const userId = uuid() const newCountryCode = 'nl' const wrapped = test.wrap(handleUserUpdates) - await db.collection(DB_ENDPOINTS.howtos).add({ + await db.collection(DB_ENDPOINTS.library).add({ _createdBy: uuid(), creatorCountry: 'uk', comments: [ @@ -129,7 +129,7 @@ describe('userUpdates', () => { ], }) - await db.collection(DB_ENDPOINTS.howtos).add({ + await db.collection(DB_ENDPOINTS.library).add({ _createdBy: userId, creatorCountry: 'uk', comments: [ @@ -158,24 +158,24 @@ describe('userUpdates', () => { ) // Assert - const howtoDocuments = await db - .collection(DB_ENDPOINTS.howtos) + const libraryDocuments = await db + .collection(DB_ENDPOINTS.library) .where('comments', 'array-contains', { _creatorId: userId, creatorCountry: newCountryCode, }) - .get() - const doc = ( - await db - .collection(DB_ENDPOINTS.howtos) - .where('comments', 'array-contains', { - _creatorId: userId, - creatorCountry: newCountryCode, - }) - .get() - )?.docs[0]?.data() - - expect(howtoDocuments.docs).toHaveLength(2) + .get()( + await db + .collection(DB_ENDPOINTS.library) + .where('comments', 'array-contains', { + _creatorId: userId, + creatorCountry: newCountryCode, + }) + .get(), + ) + ?.docs[0]?.data() + + expect(libraryDocuments.docs).toHaveLength(2) }) }) diff --git a/functions/src/userUpdates/index.ts b/functions/src/userUpdates/index.ts index 6ff3ce6e11..15e519b2f2 100644 --- a/functions/src/userUpdates/index.ts +++ b/functions/src/userUpdates/index.ts @@ -13,7 +13,7 @@ import type { IDBDocChange } from '../models' * Side-effects to be carried out on various user updates, namely: * - create user revision history * - update map pin location on change - * - update howTo creator flag on change + * - update library creator flag on change * - update userName and flag on any comments made by user *********************************************************************/ export const handleUserUpdates = functions @@ -44,7 +44,7 @@ async function updateDocuments(change: IDBDocChange) { const didChangeCountry = isUserCountryDifferent(prevInfo, info) if (didChangeCountry) { - // Update country flag in user's created howTos + // Update country flag in user's created projects const country = newCountryCode ? newCountryCode : newCountry.toLowerCase() await updatePostsCountry(info._id, country) } @@ -63,36 +63,31 @@ async function updatePostsCountry(userId: string, country: IUserDB['country']) { console.error('Missing information to set creatorCountry') return false } - console.log( - "Updating howto's, researches and question from user", - userId, - 'to country:', - country, - ) - - // 1. Update howTos - let updatedHowToCount = 0 - const howToQuerySnapshot = await db - .collection(DB_ENDPOINTS.howtos) + console.log('Updating items from user', userId, 'to country:', country) + + // 1. Update Library + let updatedCount = 0 + const querySnapshot = await db + .collection(DB_ENDPOINTS.library) .where('_createdBy', '==', userId) .get() - if (howToQuerySnapshot) { - for (const doc of howToQuerySnapshot.docs) { + if (querySnapshot) { + for (const doc of querySnapshot.docs) { try { await doc.ref.update({ creatorCountry: country, _modified: new Date().toISOString(), }) - updatedHowToCount += 1 + updatedCount += 1 } catch (error) { - console.error('Error updating HowToCountry: ', error) + console.error('Error updating Library Country: ', error) } } } else { - console.log('Error getting user howTo') + console.log('Error getting user library items') } - console.log('Successfully updated', updatedHowToCount, 'howTos!') + console.log('Successfully updated', updatedCount, 'library projects!') // 2. Update Researches let updatedResearchCount = 0 diff --git a/packages/components/src/ModerationStatus/ModerationStatus.stories.tsx b/packages/components/src/ModerationStatus/ModerationStatus.stories.tsx index 64a51f04a8..d57c61c3b3 100644 --- a/packages/components/src/ModerationStatus/ModerationStatus.stories.tsx +++ b/packages/components/src/ModerationStatus/ModerationStatus.stories.tsx @@ -19,7 +19,7 @@ export const Awaiting: StoryFn = () => ( ) export const HowtoDraft: StoryFn = () => ( - + ) export const Accepted: StoryFn = () => ( diff --git a/packages/components/src/ModerationStatus/ModerationStatus.tsx b/packages/components/src/ModerationStatus/ModerationStatus.tsx index 1e497d5303..04c6323f63 100644 --- a/packages/components/src/ModerationStatus/ModerationStatus.tsx +++ b/packages/components/src/ModerationStatus/ModerationStatus.tsx @@ -4,7 +4,7 @@ import type { ThemeUIStyleObject } from 'theme-ui' export interface Props { status: string - contentType: 'event' | 'howto' | 'research' | 'question' + contentType: 'event' | 'library' | 'research' | 'question' sx?: ThemeUIStyleObject } @@ -26,7 +26,9 @@ export const ModerationStatus = (props: Props) => { switch (status) { case 'rejected': moderationMessage = - 'howto' === contentType ? 'Needs to improve to be accepted' : 'Rejected' + 'library' === contentType + ? 'Needs to improve to be accepted' + : 'Rejected' break case 'draft': moderationMessage = 'Draft' diff --git a/packages/cypress/src/data/index.ts b/packages/cypress/src/data/index.ts index 08b3ad3676..c24d3a42f6 100644 --- a/packages/cypress/src/data/index.ts +++ b/packages/cypress/src/data/index.ts @@ -3,7 +3,7 @@ * Can be imported locally as individual namespaces or combined * @example * ``` - * import { howtos } from '../data' + * import { library } from '../data' * ``` * or * ``` diff --git a/packages/cypress/src/integration/library/discussions.spec.ts b/packages/cypress/src/integration/library/discussions.spec.ts index f39e644335..88efafd009 100644 --- a/packages/cypress/src/integration/library/discussions.spec.ts +++ b/packages/cypress/src/integration/library/discussions.spec.ts @@ -7,9 +7,9 @@ import { MOCK_DATA } from '../../data' import { howto } from '../../fixtures/library' import { generateNewUserDetails } from '../../utils/TestUtils' -const howtos = Object.values(MOCK_DATA.howtos) +const library = Object.values(MOCK_DATA.library) -const item = howtos[0] +const item = library[0] const howtoDiscussion = Object.values(MOCK_DATA.discussions).find( ({ sourceId }) => sourceId === item._id, ) diff --git a/packages/cypress/src/integration/library/read.spec.ts b/packages/cypress/src/integration/library/read.spec.ts index ab57f78941..bf058586ab 100644 --- a/packages/cypress/src/integration/library/read.spec.ts +++ b/packages/cypress/src/integration/library/read.spec.ts @@ -2,25 +2,25 @@ import { DifficultyLevel } from 'oa-shared' import { MOCK_DATA } from '../../data' -const howtos = Object.values(MOCK_DATA.howtos) +const library = Object.values(MOCK_DATA.library) describe('[How To]', () => { beforeEach(() => { cy.visit('/library') }) - describe('[List how-tos]', () => { + describe('[List Library Projects]', () => { it('[By Everyone]', () => { cy.step('Has expected page title') cy.title().should('include', `Library`) cy.step('Can search for items') - cy.get('[data-cy=howtos-search-box]').click().type('beams') + cy.get('[data-cy=library-search-box]').click().type('beams') cy.get('[data-cy=card]').its('length').should('be.eq', 1) cy.step('All basic info displayed on each card') - const howtoSlug = 'make-glass-like-beams' - const howtoUrl = `/library/${howtoSlug}` + const projectSlug = 'make-glass-like-beams' + const projectUrl = `/library/${projectSlug}` const coverFileRegex = /howto-beams-glass-0-3.jpg/ cy.get('[data-cy=card]').within(() => { @@ -28,7 +28,7 @@ describe('[How To]', () => { cy.get('img').should('have.attr', 'src').and('match', coverFileRegex) cy.get('[data-cy=Username]').contains('howto_creator') cy.get('[data-cy=category]').contains('Guides') - cy.get('a').should('have.attr', 'href').and('eq', howtoUrl) + cy.get('a').should('have.attr', 'href').and('eq', projectUrl) }) cy.step('Can clear search') @@ -47,24 +47,24 @@ describe('[How To]', () => { }) describe('[Read a project]', () => { - const specificHowtoUrl = '/library/make-an-interlocking-brick' + const itemUrl = '/library/make-an-interlocking-brick' const coverFileRegex = /brick-12-1.jpg/ describe('[By Everyone]', () => { it('[See all info]', () => { - const howto = howtos[0] + const item = library[0] // Hack to avoid flaky test as the tags are not being loaded on time - cy.queryDocuments('howtos', '_id', '==', howto._id) + cy.queryDocuments('library', '_id', '==', item._id) cy.step('Old url pattern redirects to the new location') cy.visit('/library/make-an-interlocking-brick') - cy.url().should('include', specificHowtoUrl) + cy.url().should('include', itemUrl) cy.step('Edit button is not available') cy.get('[data-cy=edit]').should('not.exist') cy.step('Project has basic info') - cy.title().should('eq', `${howto.title} - Library - Precious Plastic`) + cy.title().should('eq', `${item.title} - Library - Precious Plastic`) cy.get('[data-cy=how-to-basis]').then(($summary) => { expect($summary).to.contain('howto_creator', 'Author') expect($summary).to.contain('Last update', 'Edit') @@ -99,14 +99,14 @@ describe('[How To]', () => { cy.get('[data-cy=breadcrumbsItem]') .eq(1) - .should('contain', howto.category.label) + .should('contain', item.category.label) cy.get('[data-cy=breadcrumbsItem]') .eq(1) .children() .should('have.attr', 'href') - .and('equal', `/library?category=${howto.category._id}`) + .and('equal', `/library?category=${item.category._id}`) - cy.get('[data-cy=breadcrumbsItem]').eq(2).should('contain', howto.title) + cy.get('[data-cy=breadcrumbsItem]').eq(2).should('contain', item.title) cy.step('Download file button should redirect to sign in') cy.get('div[data-tooltip-content="Login to download"]') @@ -116,7 +116,7 @@ describe('[How To]', () => { .should('include', 'sign-in') cy.step('All steps are shown') - cy.visit(specificHowtoUrl) + cy.visit(itemUrl) cy.get('[data-cy^=step_]').should('have.length', 12) cy.step('All step info is shown') @@ -162,7 +162,7 @@ describe('[How To]', () => { describe('[By Authenticated]', () => { it('[Allows opening of attachments]', () => { cy.signUpNewUser() - cy.visit(specificHowtoUrl) + cy.visit(itemUrl) cy.step('[Presents the donation request before opening of attachments]') cy.step('Shows modal') cy.wait(2000) @@ -181,7 +181,7 @@ describe('[How To]', () => { describe('[By Owner]', () => { beforeEach(() => { - cy.visit(specificHowtoUrl) + cy.visit(itemUrl) cy.login('howto_creator@test.com', 'test1234') }) @@ -196,14 +196,14 @@ describe('[How To]', () => { cy.get('[data-cy=edit]') .click() .url() - .should('include', `${specificHowtoUrl}/edit`) + .should('include', `${itemUrl}/edit`) }) }) describe('[By Admin]', () => { beforeEach(() => { cy.login('demo_admin@example.com', 'demo_admin') - cy.visit(specificHowtoUrl) + cy.visit(itemUrl) }) it('[Delete button is visible]', () => { @@ -215,9 +215,10 @@ describe('[How To]', () => { }) describe('[Read a soft-deleted How-to]', () => { - const deletedHowtoUrl = '/library/deleted-how-to' + const deletedUrl = '/library/deleted-how-to' + beforeEach(() => { - cy.visit(deletedHowtoUrl) + cy.visit(deletedUrl) }) describe('[By Everyone]', () => { @@ -233,7 +234,7 @@ describe('[How To]', () => { describe('[By Owner]', () => { beforeEach(() => { cy.login('demo_user@example.com', 'demo_user') - cy.visit(deletedHowtoUrl) + cy.visit(deletedUrl) }) it('[Delete Button is disabled]', () => { @@ -246,7 +247,7 @@ describe('[How To]', () => { describe('[By Admin]', () => { beforeEach(() => { cy.login('demo_user@example.com', 'demo_user') - cy.visit(deletedHowtoUrl) + cy.visit(deletedUrl) }) it('[Delete Button is disabled]', () => { @@ -258,10 +259,10 @@ describe('[How To]', () => { }) describe('[Fail to find a project]', () => { - const howToNotFoundUrl = `/library/this-project-does-not-exist` + const notFoundUrl = `/library/this-project-does-not-exist` it('[Redirects to search]', () => { - cy.visit(howToNotFoundUrl) + cy.visit(notFoundUrl) cy.get('[data-test="NotFound: Heading"').should('be.visible') }) }) diff --git a/packages/cypress/src/integration/library/seo.spec.ts b/packages/cypress/src/integration/library/seo.spec.ts index 36d248a9ca..ee1d882564 100644 --- a/packages/cypress/src/integration/library/seo.spec.ts +++ b/packages/cypress/src/integration/library/seo.spec.ts @@ -3,7 +3,7 @@ import { MOCK_DATA } from '../../data' describe('[Library]', () => { describe('[SEO Metadata]', () => { const { slug, title, description, cover_image } = - MOCK_DATA.howtos.cmMzzlQP00fCckYIeL2e + MOCK_DATA.library.cmMzzlQP00fCckYIeL2e const pageTitle = `${title} - Library - Precious Plastic` diff --git a/packages/cypress/src/integration/library/write.spec.ts b/packages/cypress/src/integration/library/write.spec.ts index e82c03b6a8..7919dfb0fb 100644 --- a/packages/cypress/src/integration/library/write.spec.ts +++ b/packages/cypress/src/integration/library/write.spec.ts @@ -217,7 +217,7 @@ describe('[Library]', () => { cy.fillIntroTitle(`qwerty ${randomId}`) cy.get('[data-cy=draft]').click() const firstSlug = `/library/qwerty-${randomId}` - cy.get('[data-cy=view-howto]:enabled', { timeout: 20000 }) + cy.get('[data-cy=view-project]:enabled', { timeout: 20000 }) .click() .url() .should('include', firstSlug) @@ -278,13 +278,13 @@ describe('[Library]', () => { cy.step('A full draft was saved') cy.get('[data-cy=draft]').click() - cy.get('[data-cy=view-howto]:enabled', { timeout: 20000 }).click() + cy.get('[data-cy=view-project]:enabled', { timeout: 20000 }).click() cy.step('A full draft can be submitted for review') cy.get('[data-cy=edit]').click() cy.get('[data-cy=submit]').click() - cy.get('[data-cy=view-howto]:enabled', { timeout: 20000 }) + cy.get('[data-cy=view-project]:enabled', { timeout: 20000 }) .click() .url() .should('include', `/library/${slug}`) diff --git a/packages/cypress/src/support/db/endpoints.ts b/packages/cypress/src/support/db/endpoints.ts index 7ea72dac51..e83a4371f7 100644 --- a/packages/cypress/src/support/db/endpoints.ts +++ b/packages/cypress/src/support/db/endpoints.ts @@ -5,7 +5,7 @@ import { generateDBEndpoints } from 'oa-shared' * current implementation * @example * ``` - * const allHowtos = await db.get(DB_ENDPOINTS.howtos) + * const allLibraryProjects = await db.get(DB_ENDPOINTS.library) * ``` */ export const DB_ENDPOINTS = generateDBEndpoints() diff --git a/shared/mocks/data/index.ts b/shared/mocks/data/index.ts index 52ecf03891..c5e96477bd 100644 --- a/shared/mocks/data/index.ts +++ b/shared/mocks/data/index.ts @@ -1,6 +1,6 @@ export { categories } from './categories' export { discussions } from './discussions' -export { howtos } from './library' +export { library } from './library' export { mappins } from './mappins' export { questionCategories } from './questionCategories' export { questions } from './questions' diff --git a/shared/mocks/data/library.ts b/shared/mocks/data/library.ts index ff42cba5ce..7044dd0e97 100644 --- a/shared/mocks/data/library.ts +++ b/shared/mocks/data/library.ts @@ -1,6 +1,11 @@ -export const howtos = { +import type { DifficultyLevel, ILibrary, IModerationStatus } from '../../models' + +type LibraryList = { [id: string]: ILibrary.DB } + +export const library: LibraryList = { gPpPDEvfNT9a6w5FWzaj: { - moderation: 'accepted', + moderation: 'accepted' as IModerationStatus, + mentions: [], _id: 'gPpPDEvfNT9a6w5FWzaj', _createdBy: 'howto_creator', tags: { @@ -314,7 +319,7 @@ export const howtos = { 'https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fv2_howtos%2Fme5Bq0wq5FdoJUY8gELN%2Fhowto-bope%20brick-12-1.jpg?alt=media&token=457a344a-571c-4eb7-8b59-63bdd769eb36', contentType: 'image/jpeg', }, - difficulty_level: 'Hard', + difficulty_level: 'Hard' as DifficultyLevel, files: [ { size: 384323, @@ -344,6 +349,7 @@ export const howtos = { '0EwdD1IMrwXcOncqVwOl': { slug: 'set-up-devsite-to-help-coding', previousSlugs: ['set-up-devsite-to-help-coding'], + mentions: [], _deleted: false, time: '< 1 week', description: @@ -360,10 +366,11 @@ export const howtos = { updated: '2019-07-08T07:24:14.736Z', }, _id: '0EwdD1IMrwXcOncqVwOl', - difficulty_level: 'Easy', + difficulty_level: 'Easy' as DifficultyLevel, files: [], fileLink: 'http://google.com/', total_downloads: 10, + totalComments: 0, steps: [ { title: 'Get the code', @@ -478,20 +485,24 @@ export const howtos = { title: 'Have fun 🤙', }, ], - moderation: 'accepted', - id: 'DBgbCKle7h4CcNxeUP2V', + moderation: 'accepted' as IModerationStatus, _createdBy: 'howto_editor', tags: {}, category: { label: 'product', + _id: 'project', + _created: '2019-09-16T17:54:19.978Z', + _deleted: false, }, title: 'Set up devsite to help coding!', _modified: '2019-09-16T17:54:19.978Z', _created: '2019-07-08T07:24:24.456Z', }, I6qwynw8c9AqnpYupZnp: { - difficulty_level: 'Medium', + difficulty_level: 'Medium' as DifficultyLevel, files: [], + mentions: [], + totalComments: 2, steps: [ { title: 'Gather your materials ', @@ -1364,15 +1375,18 @@ export const howtos = { title: 'Finish the raincoat', }, ], - moderation: 'accepted', + moderation: 'accepted' as IModerationStatus, _createdBy: 'howto_creator', tags: { g36hWyk3OckrLSH1ehdIE: true, }, category: { label: 'product', + _deleted: false, + _modified: '2019-04-15T18:51:56.479Z', + _created: '2018-11-29T12:56:47.901Z', + _id: 'categoryNtr9asrGucgt7JKdRpc', }, - id: 'oIVF73weEIlVJQYfolcg', title: 'Make a raincoat with plastic bags', _modified: '2019-09-27T13:52:31.679Z', _created: '2019-06-26T14:58:44.067Z', @@ -1412,7 +1426,9 @@ export const howtos = { contentType: 'image/jpeg', }, _id: 'MCM9YtFY8mbIrVueCQ3p', - difficulty_level: 'Medium', + difficulty_level: 'Medium' as DifficultyLevel, + mentions: [], + totalComments: 2, files: [], steps: [ { @@ -1836,7 +1852,7 @@ export const howtos = { ], }, ], - moderation: 'accepted', + moderation: 'accepted' as IModerationStatus, _createdBy: 'howto_creator', tags: { Cdg3VhG5Yv9BjBEDMcH8: true, @@ -1845,8 +1861,11 @@ export const howtos = { }, category: { label: 'product', + _deleted: false, + _modified: '2019-04-15T18:51:56.479Z', + _created: '2018-11-29T12:56:47.901Z', + _id: 'categoryNtr9asrGucgt7JKdRpc', }, - id: 'DUjTZCQJ6OmSMkEYdZ6f', title: 'Make a handplane (simple mould)', _modified: '2019-10-04T14:02:46.083Z', _created: '2019-05-22T16:27:27.010Z', @@ -1854,7 +1873,6 @@ export const howtos = { previousSlugs: ['make-a-handplane-simple-mould'], }, OgxS9mtt3ZugcTzEPEXt: { - id: 'fUBiAC99ciXpvPcfJ0Fk', _createdBy: 'howto_creator', tags: { DJN99ErXz8FHy035YdMO: true, @@ -1862,7 +1880,13 @@ export const howtos = { }, category: { label: 'injection', + _deleted: false, + _modified: '2019-04-15T18:51:56.479Z', + _created: '2018-11-29T12:56:47.901Z', + _id: 'categoryNtr9asrGucgt7JKdRpc', }, + mentions: [], + totalComments: 2, title: 'Create an extruded lamp', _modified: '2019-09-27T10:28:06.541Z', _created: '2019-06-21T10:42:50.592Z', @@ -1885,7 +1909,7 @@ export const howtos = { 'https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2FhowtosV1%2FfUBiAC99ciXpvPcfJ0Fk%2Fhow-to-extruded-lamp-31.png?alt=media&token=966219f8-c487-477a-a8f7-b97ed21dbf1d', }, _id: 'OgxS9mtt3ZugcTzEPEXt', - difficulty_level: 'Medium', + difficulty_level: 'Medium' as DifficultyLevel, files: [], steps: [ { @@ -2321,7 +2345,7 @@ export const howtos = { ], }, ], - moderation: 'accepted', + moderation: 'accepted' as IModerationStatus, }, Xxz9DJdfWl03DdbK5acB: { title: 'Create bottle top earrings', @@ -2345,7 +2369,9 @@ export const howtos = { fullPath: 'uploads/howtosV1/EmJEqraybHdZpkRu6FdB/how-to-bori-15.png', }, _id: 'Xxz9DJdfWl03DdbK5acB', - difficulty_level: 'Easy', + difficulty_level: 'Easy' as DifficultyLevel, + mentions: [], + totalComments: 5, files: [], steps: [ { @@ -2565,8 +2591,7 @@ export const howtos = { ], }, ], - moderation: 'accepted', - id: 'EmJEqraybHdZpkRu6FdB', + moderation: 'accepted' as IModerationStatus, _createdBy: 'howto_creator', tags: { g36hWyk3OckrLSH1ehdIE: true, @@ -2574,6 +2599,10 @@ export const howtos = { }, category: { label: 'injection', + _deleted: false, + _modified: '2019-04-15T18:51:56.479Z', + _created: '2018-11-29T12:56:47.901Z', + _id: 'categoryNtr9asrGucgt7JKdRpc', }, }, cmMzzlQP00fCckYIeL2e: { @@ -2592,7 +2621,9 @@ export const howtos = { size: 36389, }, _id: 'cmMzzlQP00fCckYIeL2e', - difficulty_level: 'Easy', + difficulty_level: 'Easy' as DifficultyLevel, + mentions: [], + totalComments: 4, files: [], steps: [ { @@ -2839,13 +2870,17 @@ export const howtos = { _animationKey: 'uniquemjlfze', }, ], - moderation: 'accepted', + moderation: 'accepted' as IModerationStatus, _createdBy: 'howto_creator', tags: { g2rhzzwstNhU62CUs9ak: true, }, category: { label: 'product', + _deleted: false, + _modified: '2019-04-15T18:51:56.479Z', + _created: '2018-11-29T12:56:47.901Z', + _id: 'categoryNtr9asrGucgt7JKdRpc', }, title: 'Make glass-like beams', _modified: '2019-09-18T21:53:33.211Z', @@ -2862,6 +2897,7 @@ export const howtos = { _id: 'zVVjXc34gKhUNIcgLEXg', _modified: '2022-01-06T14:50:38.830Z', totalComments: 0, + mentions: [], cover_image: { contentType: 'image/jpeg', downloadUrl: @@ -2876,9 +2912,9 @@ export const howtos = { }, creatorCountry: '', description: 'Test1', - difficulty_level: 'Medium', + difficulty_level: 'Medium' as DifficultyLevel, files: [], - moderation: 'accepted', + moderation: 'accepted' as IModerationStatus, slug: 'testing-testing', previousSlugs: ['testing-testing'], steps: [ @@ -2890,11 +2926,9 @@ export const howtos = { 'https://www.youtube.com/watch?v=dQw4w9WgXcQ&ab_channel=RickAstley', }, ], - tags: [ - { - uruGBJvokGat2aCvZW0J: true, - }, - ], + tags: { + uruGBJvokGat2aCvZW0J: true, + }, time: '< 1 hour', title: 'Testing-testing', }, @@ -2904,6 +2938,8 @@ export const howtos = { _deleted: true, _id: 'zVVjXc34gKhIBVcgLEXg', _modified: '2022-01-06T14:50:38.830Z', + mentions: [], + previousSlugs: ['deleted-how-to'], cover_image: { contentType: 'image/jpeg', downloadUrl: @@ -2918,9 +2954,9 @@ export const howtos = { }, creatorCountry: '', description: 'deleted how to', - difficulty_level: 'Medium', + difficulty_level: 'Medium' as DifficultyLevel, files: [], - moderation: 'accepted', + moderation: 'accepted' as IModerationStatus, slug: 'deleted-how-to', steps: [ { @@ -2931,11 +2967,7 @@ export const howtos = { 'https://www.youtube.com/watch?v=dQw4w9WgXcQ&ab_channel=RickAstley', }, ], - tags: [ - { - uruGBJvokGat2aCvZW0J: true, - }, - ], + tags: { uruGBJvokGat2aCvZW0J: true }, time: '< 1 hour', title: 'Deleted how to', totalComments: 0, diff --git a/shared/models/db.ts b/shared/models/db.ts index 292f6be357..5d04ca3134 100644 --- a/shared/models/db.ts +++ b/shared/models/db.ts @@ -11,7 +11,7 @@ import type { ISODateString } from './common' * In the future all endpoints should try to just retain prefix-base-revision, e.g. oa_users_rev20201012 **************************************************************************************/ export const generateDBEndpoints = () => ({ - howtos: `v3_howtos`, + library: `v3_howtos`, users: `v3_users`, user_notifications: `user_notifications_rev20221209`, tags: `v3_tags`, @@ -30,11 +30,11 @@ export const generateDBEndpoints = () => ({ /** * A list of known subcollections used in endpoints, e.g. - * /howtos/my-howto-id/stats + * /library/my-project-id/stats */ export const dbEndpointSubcollections = { user: ['revisions'], - howtos: ['stats'], + library: ['stats'], research: ['stats'], } // React apps populate a process variable, however it might not always be accessible outside @@ -50,7 +50,7 @@ if (!('process' in globalThis)) { * current implementation * @example * ``` - * const allHowtos = await db.get(DB_ENDPOINTS.howtos) + * const allLibraryItems = await db.get(DB_ENDPOINTS.library) * ``` */ export const DB_ENDPOINTS = generateDBEndpoints() diff --git a/src/pages/Library/Content/Common/Library.form.test.tsx b/src/pages/Library/Content/Common/Library.form.test.tsx index c509814c28..e549432a1a 100644 --- a/src/pages/Library/Content/Common/Library.form.test.tsx +++ b/src/pages/Library/Content/Common/Library.form.test.tsx @@ -5,11 +5,11 @@ import { act, fireEvent, render, waitFor } from '@testing-library/react' import { ThemeProvider } from '@theme-ui/core' import { Provider } from 'mobx-react' import { useCommonStores } from 'src/common/hooks/useCommonStores' -import { FactoryHowto } from 'src/test/factories/Library' +import { FactoryLibraryItem } from 'src/test/factories/Library' import { testingThemeStyles } from 'src/test/utils/themeUtils' import { describe, expect, it, vi } from 'vitest' -import { HowtoForm } from './Library.form' +import { LibraryForm } from './Library.form' import type { ILibrary } from 'oa-shared' import type { ParentType } from './Library.form' @@ -20,7 +20,7 @@ vi.mock('src/common/hooks/useCommonStores', () => { return { useCommonStores: () => ({ stores: { - howtoStore: { + LibraryStore: { uploadStatus: { Start: false, Cover: false, @@ -30,7 +30,7 @@ vi.mock('src/common/hooks/useCommonStores', () => { Complete: false, }, validateTitleForSlug: vi.fn(), - uploadHowTo: vi.fn(), + upload: vi.fn(), }, tagsStore: { allTags: [ @@ -49,7 +49,7 @@ describe('Howto form', () => { describe('Provides user information', () => { it('shows maximum file size', () => { // Arrange - const formValues = FactoryHowto() + const formValues = FactoryLibraryItem() // Act let wrapper act(() => { @@ -64,7 +64,7 @@ describe('Howto form', () => { describe('Invalid file warning', () => { it('Does not appear when submitting only fileLink', () => { // Arrange - const formValues = FactoryHowto({ fileLink: 'www.test.com' }) + const formValues = FactoryLibraryItem({ fileLink: 'www.test.com' }) // Act let wrapper act(() => { @@ -79,7 +79,7 @@ describe('Howto form', () => { it('Does not appear when submitting only files', () => { // Arrange - const formValues = FactoryHowto({ + const formValues = FactoryLibraryItem({ files: [ new File(['test file content'], 'test-file.pdf', { type: 'application/pdf', @@ -101,7 +101,7 @@ describe('Howto form', () => { it('Appears when submitting 2 file types', () => { // Arrange - const formValues = FactoryHowto({ + const formValues = FactoryLibraryItem({ files: [ new File(['test file content'], 'test-file.pdf', { type: 'application/pdf', @@ -122,7 +122,7 @@ describe('Howto form', () => { it('Does not appear when files are removed and filelink added', async () => { // Arrange - const formValues = FactoryHowto({ + const formValues = FactoryLibraryItem({ files: [ new File(['test file content'], 'test-file.pdf', { type: 'application/pdf', @@ -169,7 +169,7 @@ const Wrapper = (formValues: ILibrary.DB, parentType: ParentType, navProps) => { Component: () => ( - { - const { howtoStore } = useCommonStores().stores +export const LibraryForm = observer((props: IProps) => { + const { LibraryStore } = useCommonStores().stores const [state, setState] = useState({ formSaved: false, @@ -67,14 +69,14 @@ export const HowtoForm = observer((props: IProps) => { showInvalidFileWarning: props.formValues.files?.length > 0 && props.formValues.fileLink, }) - const [howtoSlug, setHowtoSlug] = useState('') + const [itemSlug, setItemSlug] = useState('') const { formValues, parentType } = props const { fileEditMode, showSubmitModal, showInvalidFileWarning } = state const { heading } = intro const { create, edit } = headings - const formId = 'howtoForm' + const formId = 'libraryForm' const headingText = parentType === 'create' ? create : edit const checkFilesValid = (formValues: ILibrary.FormInput) => { @@ -101,8 +103,8 @@ export const HowtoForm = observer((props: IProps) => { : IModerationStatus.AWAITING_MODERATION } logger.debug('submitting form', formValues) - const howto = await howtoStore.uploadHowTo(formValues) - howto && setHowtoSlug(howto.slug) + const howto = await LibraryStore.upload(formValues) + howto && setItemSlug(howto.slug) form.reset(formValues) } // automatically generate the slug when the title changes @@ -116,12 +118,12 @@ export const HowtoForm = observer((props: IProps) => { return ( <> {showSubmitModal && ( - { setState((state) => ({ ...state, showSubmitModal: false })) - howtoStore.resetUploadStatus() + LibraryStore.resetUploadStatus() }} /> )} @@ -173,7 +175,7 @@ export const HowtoForm = observer((props: IProps) => { - + { px={2} sx={{ flexDirection: 'column', flex: [1, 1, 4] }} > - - - - - - - + + + + + { sx={{ flexDirection: 'column', flex: [1, 1, 3] }} data-cy={'intro-cover'} > - - + + - + @@ -252,23 +254,23 @@ export const HowtoForm = observer((props: IProps) => { }} > - + - - - diff --git a/src/pages/Library/Content/Common/LibraryButtonDraft.tsx b/src/pages/Library/Content/Common/LibraryButtonDraft.tsx index 400c013cee..be483e0f88 100644 --- a/src/pages/Library/Content/Common/LibraryButtonDraft.tsx +++ b/src/pages/Library/Content/Common/LibraryButtonDraft.tsx @@ -13,7 +13,7 @@ interface IProps { submitting: boolean } -export const HowtoButtonDraft = (props: IProps) => { +export const LibraryButtonDraft = (props: IProps) => { const { form, formId, submitting } = props const { create, description } = buttons.draft diff --git a/src/pages/Library/Content/Common/LibraryButtonPublish.tsx b/src/pages/Library/Content/Common/LibraryButtonPublish.tsx index e56a3faff0..7d2b4f470e 100644 --- a/src/pages/Library/Content/Common/LibraryButtonPublish.tsx +++ b/src/pages/Library/Content/Common/LibraryButtonPublish.tsx @@ -2,7 +2,7 @@ import { Button } from 'oa-components' import { buttons } from '../../labels' -export const HowtoButtonPublish = (props) => { +export const LibraryButtonPublish = (props) => { const { form, formId, submitting } = props return ( diff --git a/src/pages/Library/Content/Common/LibraryCategory.field.tsx b/src/pages/Library/Content/Common/LibraryCategory.field.tsx index 690c225c4e..fa2276e535 100644 --- a/src/pages/Library/Content/Common/LibraryCategory.field.tsx +++ b/src/pages/Library/Content/Common/LibraryCategory.field.tsx @@ -1,23 +1,21 @@ import { useEffect, useState } from 'react' import { Field } from 'react-final-form' import { CategoriesSelectV2 } from 'src/pages/common/Category/CategoriesSelectV2' -import { - FormFieldWrapper, - HowtoCategoryGuidance, -} from 'src/pages/Library/Content/Common' +import { FormFieldWrapper } from 'src/pages/common/FormFieldWrapper' +import { LibraryCategoryGuidance } from 'src/pages/Library/Content/Common' import { intro } from 'src/pages/Library/labels' -import { howtoService } from '../../library.service' +import { libraryService } from '../../library.service' import type { SelectValue } from 'src/pages/common/Category/CategoriesSelectV2' -export const HowtoFieldCategory = () => { +export const LibraryCategoryField = () => { const { placeholder, title } = intro.category const [options, setOptions] = useState([]) useEffect(() => { const getCategories = async () => { - const categories = await howtoService.getHowtoCategories() + const categories = await libraryService.getHowtoCategories() setOptions( categories .filter((x) => !x._deleted) @@ -41,7 +39,7 @@ export const HowtoFieldCategory = () => { placeholder={placeholder || ''} categories={options} /> - + )} /> diff --git a/src/pages/Library/Content/Common/LibraryCategoryGuidance.test.tsx b/src/pages/Library/Content/Common/LibraryCategoryGuidance.test.tsx index a15313aa64..ec79844ce4 100644 --- a/src/pages/Library/Content/Common/LibraryCategoryGuidance.test.tsx +++ b/src/pages/Library/Content/Common/LibraryCategoryGuidance.test.tsx @@ -3,15 +3,15 @@ import { guidance } from 'src/pages/Library/labels' import { FactoryCategory } from 'src/test/factories/Category' import { describe, expect, it } from 'vitest' -import { HowtoCategoryGuidance } from './LibraryCategoryGuidance' -import { HowtoFormProvider } from './LibraryFormProvider' +import { LibraryCategoryGuidance } from './LibraryCategoryGuidance' +import { LibraryFormProvider } from './LibraryFormProvider' describe('HowtoCategoryGuidance', () => { it('renders expected main content when a category that exists is present', async () => { render( - - - , + + + , ) const guidanceFirstLine = guidance.moulds.main.slice(0, 40) @@ -21,9 +21,9 @@ describe('HowtoCategoryGuidance', () => { it('renders expected files content when a category that exists is present', async () => { render( - - - , + + + , ) const filesGuidance = guidance.moulds.files @@ -33,9 +33,9 @@ describe('HowtoCategoryGuidance', () => { it('renders nothing when not visible', () => { const { container } = render( - - - , + + + , ) expect(container.innerHTML).toBe('') diff --git a/src/pages/Library/Content/Common/LibraryCategoryGuidance.tsx b/src/pages/Library/Content/Common/LibraryCategoryGuidance.tsx index 663c05393c..85726c7721 100644 --- a/src/pages/Library/Content/Common/LibraryCategoryGuidance.tsx +++ b/src/pages/Library/Content/Common/LibraryCategoryGuidance.tsx @@ -9,7 +9,7 @@ interface IProps { type: 'main' | 'files' } -export const HowtoCategoryGuidance = ({ category, type }: IProps) => { +export const LibraryCategoryGuidance = ({ category, type }: IProps) => { if (!category) { return null } diff --git a/src/pages/Library/Content/Common/LibraryCoverImage.field.tsx b/src/pages/Library/Content/Common/LibraryCoverImage.field.tsx index 0bf7452bba..e9e86c95c1 100644 --- a/src/pages/Library/Content/Common/LibraryCoverImage.field.tsx +++ b/src/pages/Library/Content/Common/LibraryCoverImage.field.tsx @@ -1,13 +1,13 @@ import { Field } from 'react-final-form' import { ImageInputField } from 'src/common/Form/ImageInput.field' -import { FormFieldWrapper } from 'src/pages/Library/Content/Common/FormFieldWrapper' +import { FormFieldWrapper } from 'src/pages/common/FormFieldWrapper' import { COMPARISONS } from 'src/utils/comparisons' import { draftValidationWrapper, required } from 'src/utils/validators' import { Box, Text } from 'theme-ui' import { intro } from '../../labels' -export const HowtoFieldCoverImage = () => { +export const LibraryCoverImageField = () => { const { description, title } = intro.cover_image const name = 'cover_image' diff --git a/src/pages/Library/Content/Common/LibraryCoverImageAlt.field.tsx b/src/pages/Library/Content/Common/LibraryCoverImageAlt.field.tsx index b49c8947c1..7fd439d899 100644 --- a/src/pages/Library/Content/Common/LibraryCoverImageAlt.field.tsx +++ b/src/pages/Library/Content/Common/LibraryCoverImageAlt.field.tsx @@ -1,11 +1,11 @@ import { Field } from 'react-final-form' import { FieldInput } from 'oa-components' -import { FormFieldWrapper } from 'src/pages/Library/Content/Common/FormFieldWrapper' +import { FormFieldWrapper } from 'src/pages/common/FormFieldWrapper' import { Box, Text } from 'theme-ui' import { intro } from '../../labels' -export const HowtoFieldCoverImageAlt = () => { +export const LibraryCoverImageAltField = () => { const { description, placeholder, title } = intro.cover_image_alt const name = 'cover_image_alt' diff --git a/src/pages/Library/Content/Common/LibraryDescription.field.test.tsx b/src/pages/Library/Content/Common/LibraryDescription.field.test.tsx index 60fa9f3592..796df241b5 100644 --- a/src/pages/Library/Content/Common/LibraryDescription.field.test.tsx +++ b/src/pages/Library/Content/Common/LibraryDescription.field.test.tsx @@ -1,15 +1,15 @@ import { render, screen } from '@testing-library/react' import { describe, it } from 'vitest' -import { HowtoFieldDescription } from './LibraryDescription.field' -import { HowtoFormProvider } from './LibraryFormProvider' +import { LibraryDescriptionField } from './LibraryDescription.field' +import { LibraryFormProvider } from './LibraryFormProvider' describe('HowtoFieldStepsDescription', () => { it('renders', async () => { render( - - - , + + + , ) await screen.findByText('Short description *') diff --git a/src/pages/Library/Content/Common/LibraryDescription.field.tsx b/src/pages/Library/Content/Common/LibraryDescription.field.tsx index 7c3ad67d51..58dc8b7f6f 100644 --- a/src/pages/Library/Content/Common/LibraryDescription.field.tsx +++ b/src/pages/Library/Content/Common/LibraryDescription.field.tsx @@ -1,13 +1,13 @@ import { Field } from 'react-final-form' import { FieldTextarea } from 'oa-components' -import { FormFieldWrapper } from 'src/pages/Library/Content/Common/FormFieldWrapper' +import { FormFieldWrapper } from 'src/pages/common/FormFieldWrapper' import { COMPARISONS } from 'src/utils/comparisons' import { draftValidationWrapper, required } from 'src/utils/validators' import { HOWTO_MAX_LENGTH } from '../../constants' import { intro } from '../../labels' -export const HowtoFieldDescription = () => { +export const LibraryDescriptionField = () => { const { description, title } = intro.description const name = 'description' diff --git a/src/pages/Library/Content/Common/LibraryDifficulty.field.tsx b/src/pages/Library/Content/Common/LibraryDifficulty.field.tsx index 13ee798aeb..db49afe857 100644 --- a/src/pages/Library/Content/Common/LibraryDifficulty.field.tsx +++ b/src/pages/Library/Content/Common/LibraryDifficulty.field.tsx @@ -1,13 +1,13 @@ import { Field } from 'react-final-form' import { SelectField } from 'src/common/Form/Select.field' -import { FormFieldWrapper } from 'src/pages/Library/Content/Common/FormFieldWrapper' +import { FormFieldWrapper } from 'src/pages/common/FormFieldWrapper' import { COMPARISONS } from 'src/utils/comparisons' import { draftValidationWrapper, required } from 'src/utils/validators' import { intro } from '../../labels' import { DIFFICULTY_OPTIONS } from './FormSettings' -export const HowtoFieldDifficulty = () => { +export const LibraryDifficultyField = () => { const { placeholder, title } = intro.difficulty_level const name = 'difficulty_level' diff --git a/src/pages/Library/Content/Common/LibraryErrors.test.tsx b/src/pages/Library/Content/Common/LibraryErrors.test.tsx index 062565ad20..7ec090f565 100644 --- a/src/pages/Library/Content/Common/LibraryErrors.test.tsx +++ b/src/pages/Library/Content/Common/LibraryErrors.test.tsx @@ -1,8 +1,8 @@ import { render, screen } from '@testing-library/react' import { describe, expect, it } from 'vitest' -import { HowtoErrors } from './LibraryErrors' -import { HowtoFormProvider } from './LibraryFormProvider' +import { LibraryErrors } from './LibraryErrors' +import { LibraryFormProvider } from './LibraryFormProvider' describe('HowtoErrors', () => { it('renders component when visible and has intro errors', async () => { @@ -13,9 +13,9 @@ describe('HowtoErrors', () => { } render( - - - , + + + , ) await screen.findByText(titleError, { exact: false }) @@ -29,9 +29,9 @@ describe('HowtoErrors', () => { } render( - - - , + + + , ) await screen.findByText(text, { exact: false }) @@ -44,9 +44,9 @@ describe('HowtoErrors', () => { } const { container } = render( - - - , + + + , ) expect(container.innerHTML).toBe('') diff --git a/src/pages/Library/Content/Common/LibraryErrors.tsx b/src/pages/Library/Content/Common/LibraryErrors.tsx index 7d3fb650be..4e825fac50 100644 --- a/src/pages/Library/Content/Common/LibraryErrors.tsx +++ b/src/pages/Library/Content/Common/LibraryErrors.tsx @@ -1,6 +1,6 @@ import { ErrorsContainer } from 'src/common/Form/ErrorsContainer' -import { transformHowtoErrors } from '../utils' +import { transformLibraryErrors } from '../utils' import type { IErrorsListSet, ITopLevelErrorsList } from 'src/common/Form/types' @@ -9,8 +9,8 @@ interface IProps { isVisible: boolean } -export const HowtoErrors = ({ errors, isVisible }: IProps) => { - const errorsListSet = errors ? transformHowtoErrors(errors) : [] +export const LibraryErrors = ({ errors, isVisible }: IProps) => { + const errorsListSet = errors ? transformLibraryErrors(errors) : [] if ( !isVisible || diff --git a/src/pages/Library/Content/Common/LibraryFileLink.field.tsx b/src/pages/Library/Content/Common/LibraryFileLink.field.tsx index c65efce6f6..a55d5e04e7 100644 --- a/src/pages/Library/Content/Common/LibraryFileLink.field.tsx +++ b/src/pages/Library/Content/Common/LibraryFileLink.field.tsx @@ -1,6 +1,6 @@ import { Field } from 'react-final-form' import { FieldInput } from 'oa-components' -import { FormFieldWrapper } from 'src/pages/Library/Content/Common/FormFieldWrapper' +import { FormFieldWrapper } from 'src/pages/common/FormFieldWrapper' import { COMPARISONS } from 'src/utils/comparisons' import { draftValidationWrapper, @@ -10,7 +10,7 @@ import { import { MAX_LINK_LENGTH } from '../../constants' import { intro } from '../../labels' -export const HowtoFieldFileLink = () => { +export const LibraryFileLinkField = () => { const { description, title } = intro.fileLink const name = 'fileLink' diff --git a/src/pages/Library/Content/Common/LibraryFileUpload.field.tsx b/src/pages/Library/Content/Common/LibraryFileUpload.field.tsx index 027ffd52a0..85b9e99b8b 100644 --- a/src/pages/Library/Content/Common/LibraryFileUpload.field.tsx +++ b/src/pages/Library/Content/Common/LibraryFileUpload.field.tsx @@ -2,12 +2,12 @@ import { Field } from 'react-final-form' import { UserRole } from 'oa-shared' import { AuthWrapper } from 'src/common/AuthWrapper' import { FileInputField } from 'src/common/Form/FileInput.field' -import { FormFieldWrapper } from 'src/pages/Library/Content/Common/FormFieldWrapper' +import { FormFieldWrapper } from 'src/pages/common/FormFieldWrapper' import { Text } from 'theme-ui' import { intro } from '../../labels' -export const HowtoFieldFileUpload = () => { +export const LibraryFileUploadField = () => { const { description, title } = intro.files const name = 'files' diff --git a/src/pages/Library/Content/Common/LibraryFiles.field.test.tsx b/src/pages/Library/Content/Common/LibraryFiles.field.test.tsx index 9868b95127..1b84bff4ce 100644 --- a/src/pages/Library/Content/Common/LibraryFiles.field.test.tsx +++ b/src/pages/Library/Content/Common/LibraryFiles.field.test.tsx @@ -3,14 +3,14 @@ import { guidance } from 'src/pages/Library/labels' import { FactoryCategory } from 'src/test/factories/Category' import { describe, it, vi } from 'vitest' -import { HowtoFieldFiles } from './LibraryFiles.field' -import { HowtoFormProvider } from './LibraryFormProvider' +import { LibraryFilesField } from './LibraryFiles.field' +import { LibraryFormProvider } from './LibraryFormProvider' vi.mock('src/common/hooks/useCommonStores', () => { return { useCommonStores: () => ({ stores: { - howtoStore: { + LibraryStore: { uploadStatus: { Start: false, Cover: false, @@ -20,7 +20,7 @@ vi.mock('src/common/hooks/useCommonStores', () => { Complete: false, }, validateTitleForSlug: vi.fn(), - uploadHowTo: vi.fn(), + upload: vi.fn(), }, tagsStore: { allTags: [ @@ -46,9 +46,9 @@ describe('HowtoFieldFiles', () => { } render( - - - , + + + , ) await screen.findByText( @@ -66,9 +66,9 @@ describe('HowtoFieldFiles', () => { } render( - - - , + + + , ) await screen.findByText(guidance.moulds.files) diff --git a/src/pages/Library/Content/Common/LibraryFiles.field.tsx b/src/pages/Library/Content/Common/LibraryFiles.field.tsx index 5a44659348..b20f2b3aff 100644 --- a/src/pages/Library/Content/Common/LibraryFiles.field.tsx +++ b/src/pages/Library/Content/Common/LibraryFiles.field.tsx @@ -1,9 +1,9 @@ import { Button, DownloadStaticFile } from 'oa-components' +import { FormFieldWrapper } from 'src/pages/common/FormFieldWrapper' import { - FormFieldWrapper, - HowtoCategoryGuidance, - HowtoFieldFileLink, - HowtoFieldFileUpload, + LibraryCategoryGuidance, + LibraryFileLinkField, + LibraryFileUploadField, } from 'src/pages/Library/Content/Common' import { Flex, Text } from 'theme-ui' @@ -20,7 +20,7 @@ interface IProps { showInvalidFileWarning: boolean } -export const HowtoFieldFiles = (props: IProps) => { +export const LibraryFilesField = (props: IProps) => { const { category, onClick, showInvalidFileWarning, files, fileEditMode } = props @@ -29,13 +29,13 @@ export const HowtoFieldFiles = (props: IProps) => { {showInvalidFileWarning && } - + {files?.length && !fileEditMode ? ( ) : ( <> - - + + )} diff --git a/src/pages/Library/Content/Common/LibraryFormProvider.tsx b/src/pages/Library/Content/Common/LibraryFormProvider.tsx index 978e359c5e..935409b51c 100644 --- a/src/pages/Library/Content/Common/LibraryFormProvider.tsx +++ b/src/pages/Library/Content/Common/LibraryFormProvider.tsx @@ -1,15 +1,15 @@ import { Form } from 'react-final-form' import arrayMutators from 'final-form-arrays' -import { FactoryHowto } from 'src/test/factories/Library' +import { FactoryLibraryItem } from 'src/test/factories/Library' import { vi } from 'vitest' -export const HowtoFormProvider = ({ +export const LibraryFormProvider = ({ children, }: { children: React.ReactNode }) => { const formProps = { - formValues: FactoryHowto(), + formValues: FactoryLibraryItem(), onSubmit: vi.fn(), mutators: { ...arrayMutators }, component: () => children, diff --git a/src/pages/Library/Content/Common/LibraryPostingGuidelines.tsx b/src/pages/Library/Content/Common/LibraryPostingGuidelines.tsx index c27595ce2d..aecbaa215e 100644 --- a/src/pages/Library/Content/Common/LibraryPostingGuidelines.tsx +++ b/src/pages/Library/Content/Common/LibraryPostingGuidelines.tsx @@ -1,6 +1,6 @@ import { ExternalLink, Guidelines } from 'oa-components' -export const HowtoPostingGuidelines = () => ( +export const LibraryPostingGuidelines = () => ( { +describe('LibraryStepField', () => { it('renders', async () => { const props = { step: [], @@ -15,9 +15,9 @@ describe('HowtoFieldStep', () => { } render( - - - , + + + , ) await screen.findByText('Step 1 *') diff --git a/src/pages/Library/Content/Common/LibraryStep.field.tsx b/src/pages/Library/Content/Common/LibraryStep.field.tsx index 21cbde93ca..c79f79e058 100644 --- a/src/pages/Library/Content/Common/LibraryStep.field.tsx +++ b/src/pages/Library/Content/Common/LibraryStep.field.tsx @@ -53,7 +53,7 @@ interface IState { * - minimum character length of 100 characters * - maximum character length of 1000 characters */ -export const HowtoFieldStep = (props: IProps) => { +export const LibraryStepField = (props: IProps) => { const { step, index } = props const [state, setState] = useState({ showDeleteModal: false, @@ -96,7 +96,7 @@ export const HowtoFieldStep = (props: IProps) => { const isAboveMinimumStep = index >= HOWTO_MIN_REQUIRED_STEPS return ( - // NOTE - animation parent container in CreateHowTo + // NOTE - animation parent container in CreateLibrary diff --git a/src/pages/Library/Content/Common/LibraryStepsContainer.field.test.tsx b/src/pages/Library/Content/Common/LibraryStepsContainer.field.test.tsx index ae82a205b0..4ea3a22d50 100644 --- a/src/pages/Library/Content/Common/LibraryStepsContainer.field.test.tsx +++ b/src/pages/Library/Content/Common/LibraryStepsContainer.field.test.tsx @@ -1,15 +1,15 @@ import { render, screen } from '@testing-library/react' import { describe, it } from 'vitest' -import { HowtoFormProvider } from './LibraryFormProvider' -import { HowtoFieldStepsContainer } from './LibraryStepsContainer.field' +import { LibraryFormProvider } from './LibraryFormProvider' +import { LibraryStepsContainerField } from './LibraryStepsContainer.field' describe('HowtoFieldStepsContainer', () => { it('renders', async () => { render( - - - , + + + , ) await screen.findByText('Add step') diff --git a/src/pages/Library/Content/Common/LibraryStepsContainer.field.tsx b/src/pages/Library/Content/Common/LibraryStepsContainer.field.tsx index 1bd05cb823..d13f5eea3a 100644 --- a/src/pages/Library/Content/Common/LibraryStepsContainer.field.tsx +++ b/src/pages/Library/Content/Common/LibraryStepsContainer.field.tsx @@ -1,7 +1,7 @@ import { FieldArray } from 'react-final-form-arrays' import { AnimatePresence, motion } from 'framer-motion' import { Button } from 'oa-components' -import { HowtoFieldStep } from 'src/pages/Library/Content/Common/LibraryStep.field' +import { LibraryStepField } from 'src/pages/Library/Content/Common/LibraryStep.field' import { COMPARISONS } from 'src/utils/comparisons' import { Box, Flex, Heading, Text } from 'theme-ui' @@ -40,7 +40,7 @@ const AnimationContainer = ({ children }: IPropsAnimation) => { ) } -export const HowtoFieldStepsContainer = () => { +export const LibraryStepsContainerField = () => { return ( {({ fields }) => ( @@ -59,7 +59,7 @@ export const HowtoFieldStepsContainer = () => { - { +export const LibraryTagsField = () => { return ( { +export const LibraryTimeField = () => { const { placeholder, title } = intro.time const name = 'time' diff --git a/src/pages/Library/Content/Common/LibraryTitle.field.test.tsx b/src/pages/Library/Content/Common/LibraryTitle.field.test.tsx index 45b35378c9..0b2be57652 100644 --- a/src/pages/Library/Content/Common/LibraryTitle.field.test.tsx +++ b/src/pages/Library/Content/Common/LibraryTitle.field.test.tsx @@ -1,27 +1,27 @@ import { render, screen } from '@testing-library/react' import { describe, it, vi } from 'vitest' -import { HowtoFormProvider } from './LibraryFormProvider' -import { HowtoFieldTitle } from './LibraryTitle.field' +import { LibraryFormProvider } from './LibraryFormProvider' +import { LibraryTitleField } from './LibraryTitle.field' -import type { HowtoStore } from 'src/stores/Library/library.store' +import type { LibraryStore } from 'src/stores/Library/library.store' import type { ParentType } from './Library.form' vi.mock('src/stores/Library/library.store') const store = await vi.importMock('src/stores/Library/library.store') -describe('HowtoFieldTitle', () => { +describe('LibraryTitleField', () => { it('renders', async () => { const props = { _id: 'random-123', parentType: 'create' as ParentType, - store: store as any as HowtoStore, + store: store as any as LibraryStore, } render( - - - , + + + , ) await screen.findByText('0 / 50') diff --git a/src/pages/Library/Content/Common/LibraryTitle.field.tsx b/src/pages/Library/Content/Common/LibraryTitle.field.tsx index 3cd487e0e4..722ba4c997 100644 --- a/src/pages/Library/Content/Common/LibraryTitle.field.tsx +++ b/src/pages/Library/Content/Common/LibraryTitle.field.tsx @@ -1,7 +1,7 @@ import { useState } from 'react' import { Field } from 'react-final-form' import { FieldInput } from 'oa-components' -import { FormFieldWrapper } from 'src/pages/Library/Content/Common/FormFieldWrapper' +import { FormFieldWrapper } from 'src/pages/common/FormFieldWrapper' import { COMPARISONS } from 'src/utils/comparisons' import { composeValidators, minValue, required } from 'src/utils/validators' import { Card, Text } from 'theme-ui' @@ -9,14 +9,14 @@ import { Card, Text } from 'theme-ui' import { HOWTO_TITLE_MAX_LENGTH, HOWTO_TITLE_MIN_LENGTH } from '../../constants' import { intro } from '../../labels' -import type { HowtoStore } from 'src/stores/Library/library.store' +import type { LibraryStore } from 'src/stores/Library/library.store' interface IProps { - store: HowtoStore + store: LibraryStore _id: string } -export const HowtoFieldTitle = (props: IProps) => { +export const LibraryTitleField = (props: IProps) => { const { store, _id } = props const { placeholder, title } = intro.title diff --git a/src/pages/Library/Content/Common/SubmitStatus.tsx b/src/pages/Library/Content/Common/SubmitStatus.tsx index dc044b6806..c98df76236 100644 --- a/src/pages/Library/Content/Common/SubmitStatus.tsx +++ b/src/pages/Library/Content/Common/SubmitStatus.tsx @@ -11,11 +11,11 @@ interface IProps { onClose: () => void } -const HowToSubmitStatus = observer((props: IProps) => { +export const LibrarySubmitStatus = observer((props: IProps) => { const navigate = useNavigate() - const { howtoStore } = useCommonStores().stores + const { LibraryStore } = useCommonStores().stores - const uploadStatus = howtoStore.uploadStatus + const uploadStatus = LibraryStore.uploadStatus return ( @@ -41,7 +41,7 @@ const HowToSubmitStatus = observer((props: IProps) => { diff --git a/src/pages/Library/Content/List/LibraryListHeader.tsx b/src/pages/Library/Content/List/LibraryListHeader.tsx index e3fa7471c6..717b9f2429 100644 --- a/src/pages/Library/Content/List/LibraryListHeader.tsx +++ b/src/pages/Library/Content/List/LibraryListHeader.tsx @@ -9,11 +9,11 @@ import { ListHeader } from 'src/pages/common/Layout/ListHeader' import { Button, Flex } from 'theme-ui' import { listing } from '../../labels' -import { howtoService, HowtosSearchParams } from '../../library.service' -import { HowtoSortOptions } from './LibrarySortOptions' +import { LibrarySearchParams, libraryService } from '../../library.service' +import { LibrarySortOptions } from './LibrarySortOptions' import type { ICategory } from 'shared/lib' -import type { HowtoSortOption } from './LibrarySortOptions' +import type { LibrarySortOption } from './LibrarySortOptions' interface IProps { draftCount: number @@ -21,16 +21,16 @@ interface IProps { showDrafts: boolean } -export const HowtoHeader = (props: IProps) => { +export const LibraryListHeader = (props: IProps) => { const { draftCount, handleShowDrafts, showDrafts } = props const [categories, setCategories] = useState([]) const [searchString, setSearchString] = useState('') const [searchParams, setSearchParams] = useSearchParams() - const categoryParam = searchParams.get(HowtosSearchParams.category) + const categoryParam = searchParams.get(LibrarySearchParams.category) const category = categories?.find((cat) => cat._id === categoryParam) ?? null - const q = searchParams.get(HowtosSearchParams.q) - const sort = searchParams.get(HowtosSearchParams.sort) as HowtoSortOption + const q = searchParams.get(LibrarySearchParams.q) + const sort = searchParams.get(LibrarySearchParams.sort) as LibrarySortOption const headingTitle = import.meta.env.VITE_HOWTOS_HEADING const isUserLoggedIn = useCommonStores().stores.userStore?.user @@ -43,7 +43,7 @@ export const HowtoHeader = (props: IProps) => { useEffect(() => { const initCategories = async () => { - const categories = (await howtoService.getHowtoCategories()) || [] + const categories = (await libraryService.getHowtoCategories()) || [] const notDeletedCategories = categories.filter( ({ _deleted }) => _deleted === false, ) @@ -54,7 +54,7 @@ export const HowtoHeader = (props: IProps) => { }, []) const updateFilter = useCallback( - (key: HowtosSearchParams, value: string) => { + (key: LibrarySearchParams, value: string) => { const params = new URLSearchParams(searchParams.toString()) if (value) { params.set(key, value) @@ -75,14 +75,14 @@ export const HowtoHeader = (props: IProps) => { const searchValue = (value: string) => { const params = new URLSearchParams(searchParams.toString()) - params.set(HowtosSearchParams.q, value) + params.set(LibrarySearchParams.q, value) if (value.length > 0 && sort !== 'MostRelevant') { - params.set(HowtosSearchParams.sort, 'MostRelevant') + params.set(LibrarySearchParams.sort, 'MostRelevant') } if (value.length === 0 || !value) { - params.set(HowtosSearchParams.sort, 'Newest') + params.set(LibrarySearchParams.sort, 'Newest') } setSearchParams(params) @@ -94,7 +94,7 @@ export const HowtoHeader = (props: IProps) => { activeCategory={category} setActiveCategory={(updatedCategory) => updateFilter( - HowtosSearchParams.category, + LibrarySearchParams.category, updatedCategory ? updatedCategory._id : '', ) } @@ -106,21 +106,21 @@ export const HowtoHeader = (props: IProps) => {