diff --git a/.github/workflows/worktree-dev-test.yml b/.github/workflows/worktree-dev-test.yml new file mode 100644 index 00000000..a8ee13ee --- /dev/null +++ b/.github/workflows/worktree-dev-test.yml @@ -0,0 +1,33 @@ +name: Worktree development tooling + +on: + push: + branches: [main] + paths: + - 'scripts/worktree-dev/**' + - '.gitignore' + - 'Makefile' + - '.github/workflows/worktree-dev-test.yml' + pull_request: + paths: + - 'scripts/worktree-dev/**' + - '.gitignore' + - 'Makefile' + - '.github/workflows/worktree-dev-test.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + lifecycle: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + - name: Check syntax + run: | + bash -n scripts/worktree-dev/*.sh scripts/worktree-dev/talebound-worktree-dev + python3 -m py_compile scripts/worktree-dev/*.py + - name: Verify concurrent worktree isolation and cleanup + run: scripts/worktree-dev/test-concurrent-worktrees.sh diff --git a/.gitignore b/.gitignore index b24428df..35a60376 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,10 @@ .DS_Store .worktrees/ + +# Generated local worktree configuration and runtime state +.talebound/ +.local-dev/ +.env.worktree +backend/app.env.worktree +frontend/.env.worktree +__pycache__/ diff --git a/AGENTS.md b/AGENTS.md index a83c2da5..ff3d2ea3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,3 +20,52 @@ - Prefer an existing suitable worktree. Otherwise branch from fetched `origin/main` with an explicit task branch and destination path, using `--no-track`. Use `git worktree add` if a managed tool cannot honor the required location. + +## Local worktree development + +- Read `scripts/worktree-dev/README.md` before changing local lifecycle tooling. +- On Linux/WSL, bootstrap a new worktree with + `scripts/worktree-dev/bootstrap-worktree.sh`. This installs locked frontend + dependencies, starts this worktree's data services, and applies migrations. +- Use `scripts/worktree-dev/talebound-worktree-dev up`, `status`, `logs`, and + `down` for the app lifecycle. Root Makefile aliases are also supported. + Do not create ad-hoc shared databases or use the legacy backend Compose file. +- `setup` and `up` are idempotent; neither replaces existing database data. + `down` retains data. Before `down --purge-data`, check `status` and the exact + current worktree path. Purge deletes this worktree's labelled data and cache + volumes. Never remove unrelated or unlabelled Docker resources. +- Every worktree has a separate PostgreSQL container/volume, Redis queue, + Mailpit inbox, auth key, and loopback address/ports. Open the frontend URL + reported by `status`; substituting `localhost` breaks origin/session isolation. +- `.talebound/`, `.env.worktree`, `backend/app.env.worktree`, and + `frontend/.env.worktree` are generated local state. Never commit them, print + their secret values, or copy them between worktrees. Registry/lock files live + in the primary clone's Git metadata and must not be edited while tooling runs. +- The launcher generates development configuration and does not copy another + checkout's environment. Email stays in Mailpit, Sentry is disabled, and + Cloudflare is unconfigured by default. Optional development image credentials + belong in `.talebound/integrations.env`; never add production fallbacks. +- Frontend changes hot reload. For backend changes, use + `scripts/worktree-dev/talebound-worktree-dev restart backend`: the container + snapshots the source so migrations do not write generated SQL into the checkout. +- Purge the current stack before removing a disposable worktree. Use + `git worktree remove` for Git-created worktrees. Preserve development data when + it has not been intentionally discarded. + +## Validation + +- For lifecycle, ownership, ports, or cleanup changes: run Bash syntax checks, + Python compilation, `status`, and + `scripts/worktree-dev/test-concurrent-worktrees.sh`. This creates disposable + worktrees under this workspace and verifies isolation and scoped cleanup. +- For backend changes, use `scripts/worktree-dev/talebound-worktree-dev migrate` + when needed, then `test-backend`. Short tests skip the legacy real-email test. + Tests write fixtures to the current worktree database; never point them at a + shared or production database. +- For frontend changes, run relevant lint/build checks and verify the affected + browser flow. Report failures honestly and do not suppress checks to make a + build appear green. Use `useAppSelector`/`useAppDispatch` from + `frontend/store/hooks.ts` for typed Redux access. +- For worktree authentication/email changes, verify signup, local email delivery, + verification, login, and isolation from another running worktree. Report the + actual commands/results and any untested behavior before handoff. diff --git a/Makefile b/Makefile index e7c2005f..d00f370b 100644 --- a/Makefile +++ b/Makefile @@ -2,28 +2,47 @@ NODE ?= node FRONTEND_YARN = cd frontend && $(NODE) .yarn/releases/yarn-3.6.1.cjs -COMPOSE = docker compose -f backend/docker-compose.yaml +WORKTREE_DEV = scripts/worktree-dev/talebound-worktree-dev -.PHONY: help frontend-install frontend-dev frontend-build frontend-lint backend-dev backend-build backend-test backend-migrate api-generate services-up services-down +.PHONY: help dev-setup dev-up dev-down dev-status dev-logs frontend-install frontend-dev frontend-build frontend-lint backend-dev backend-build backend-test backend-migrate api-generate services-up services-down help: + @echo "dev-setup Install dependencies and migrate this worktree's local database" + @echo "dev-up Set up and start this worktree's complete local app" + @echo "dev-down Stop this worktree, retaining data" + @echo "dev-status Show this worktree's URLs and services" + @echo "dev-logs Show recent logs for this worktree" @echo "frontend-install Install frontend dependencies from the lockfile" - @echo "frontend-dev Start Next.js on port 4000" + @echo "frontend-dev Alias for dev-up (starts both applications)" @echo "frontend-build Build the frontend" @echo "frontend-lint Lint the frontend" - @echo "backend-dev Start the Go API" + @echo "backend-dev Alias for dev-up (starts both applications)" @echo "backend-build Compile all Go packages" - @echo "backend-test Run Go tests (requires a migrated test database)" - @echo "backend-migrate Apply database migrations to the configured database" + @echo "backend-test Run Go short tests in this worktree's container and database" + @echo "backend-migrate Apply migrations to this worktree's database" @echo "api-generate Generate frontend API types from the local Swagger schema" - @echo "services-up Start local PostgreSQL and Redis" - @echo "services-down Stop local Compose services, retaining volumes" + @echo "services-up Start this worktree's PostgreSQL, Redis, and local email inbox" + @echo "services-down Alias for dev-down" + +dev-setup: + $(WORKTREE_DEV) setup + +dev-up: + $(WORKTREE_DEV) up + +dev-down: + $(WORKTREE_DEV) down + +dev-status: + $(WORKTREE_DEV) status + +dev-logs: + $(WORKTREE_DEV) logs frontend-install: $(FRONTEND_YARN) install --immutable -frontend-dev: - $(FRONTEND_YARN) dev +frontend-dev: dev-up frontend-build: $(FRONTEND_YARN) build @@ -31,23 +50,21 @@ frontend-build: frontend-lint: $(FRONTEND_YARN) lint -backend-dev: - $(MAKE) -C backend server +backend-dev: dev-up backend-build: cd backend && go build ./... backend-test: - $(MAKE) -C backend test + $(WORKTREE_DEV) test-backend backend-migrate: - $(MAKE) -C backend migrateup + $(WORKTREE_DEV) migrate api-generate: $(FRONTEND_YARN) generate services-up: - $(COMPOSE) up -d postgres redis + $(WORKTREE_DEV) setup --services-only -services-down: - $(COMPOSE) down +services-down: dev-down diff --git a/README.md b/README.md index b53077c4..1c387e58 100644 --- a/README.md +++ b/README.md @@ -10,37 +10,34 @@ The frontend and backend for Talebound, developed together in one repository. ## Development -Use Node.js 22, Go 1.23 (as declared in `backend/go.mod`), GNU Make, and Docker -with Compose. Yarn is checked into the frontend, so a global Yarn installation -is not required. Run `make` to see the available commands. +On Linux/WSL, use Bash, Python 3.9+, Git, and Docker with Compose v2. The supported +worktree launcher runs Node.js 22, Go 1.23, PostgreSQL 15, Redis 7, and Mailpit in +containers. No host Node, Go, Yarn, or Make installation is needed to run the app. ```sh -make frontend-install -make services-up -make backend-migrate +scripts/worktree-dev/bootstrap-worktree.sh +scripts/worktree-dev/talebound-worktree-dev up +scripts/worktree-dev/talebound-worktree-dev status ``` -The backend loads `backend/app.env`. Put local overrides in the ignored -`backend/app.env.local`, which is loaded by the backend Makefile. Review the -database configuration before running migrations. The supplied development -database uses port 5432 and Redis uses port 6379. - -Create `frontend/.env.local` using the settings described in -[the frontend README](frontend/README.md), including the API URL and the service -keys needed for features you use. The existing development CORS setup uses -`dev.talebound.net`; map it to `127.0.0.1` in your hosts file. - -Start each application in its own terminal: - -```sh -make backend-dev -make frontend-dev -``` - -The frontend runs at `http://dev.talebound.net:4000` and the HTTP API at -`http://dev.talebound.net:8080`. Email, image delivery, and other integrations -still need their own configuration. See [the backend README](backend/README.md) -for its existing development tools and workflow. +Each worktree gets its own database, Redis queue, email inbox, loopback address, +ports, and authentication key. `status` prints the exact app and inbox URLs. +Use those URLs: separate loopback addresses also keep browser cookies isolated. +No hosts-file edits or AWS credentials are required. Register an account in the +app and open the verification email in the local inbox. + +Bootstrap is idempotent and stops after installing dependencies and applying +migrations; `up` starts both applications. `down` stops this worktree and retains +data. Generated `.env.worktree`, `backend/app.env.worktree`, +`frontend/.env.worktree`, and `.talebound/` are ignored local state. Do not copy +them between worktrees. Optional development Cloudflare settings go in +`.talebound/integrations.env`; uploads require those credentials. + +See [the worktree development guide](scripts/worktree-dev/README.md) for lifecycle +commands, environment precedence, database access, cleanup, and validation. +GNU Make aliases (`make dev-up`, `make dev-status`, `make dev-down`) are optional. +The older `backend/Makefile` and `backend/docker-compose.yaml` contain historical +single-repository commands; use the root launcher for local worktree development. ## Validation and API generation @@ -52,7 +49,9 @@ make backend-test make api-generate ``` -Backend tests require the migrated PostgreSQL database. API generation reads +Backend tests use the current worktree's migrated PostgreSQL database. The +frontend checks/API generator and `backend-build` use host Node.js 22/Go 1.23; +their container equivalents are documented in the worktree guide. API generation reads `backend/doc/swagger/talebound.swagger.json` directly; it no longer needs a running backend or the old development server. After changing protobuf API definitions, regenerate the backend schema with `make -C backend proto` using @@ -70,12 +69,9 @@ existing imports and protobuf package identities. Go builds use the local client without changes. The Yarn lockfile installed with `--immutable`. - GitHub Actions workflows passed `actionlint`; Compose configuration and root Makefile command paths were checked. -- The imported frontend has existing build blockers: lint reports 28 errors - and one warning, and a separate build with lint disabled fails type checking - in `frontend/api/entities/useCreateEntityGroupContent.ts` because - `entityGroupId` can be undefined. The source and generated client are unchanged - by this migration. Frontend CI will report these failures until they are fixed; - lint and type checking have not been disabled in the build configuration. +- Frontend lint, TypeScript checking, and the production build pass with Node 22 + and the checked-in Yarn lockfile. The inherited API/type mismatches and unused + code have been corrected; lint and type checking remain enabled. ## Builds and deployment diff --git a/backend/mail/sender.go b/backend/mail/sender.go index 56fe8d71..0dac7fdf 100644 --- a/backend/mail/sender.go +++ b/backend/mail/sender.go @@ -3,7 +3,9 @@ package mail import ( "fmt" "github.com/jordan-wright/email" + "net" "net/smtp" + "strings" ) const ( @@ -27,14 +29,25 @@ type AwsSesSender struct { fromEmailAddress string smtpUsername string smtpPassword string + smtpEndpoint string } func NewAwsSesSender(name string, fromEmailAddress string, smtpUsername string, smtpPassword string) EmailSender { + return NewSMTPSender(name, fromEmailAddress, smtpServerAddress, smtpUsername, smtpPassword) +} + +// NewSMTPSender accepts host:port, or a hostname using the submission port 587. +// An unauthenticated local SMTP sink needs neither username nor password. +func NewSMTPSender(name, fromEmailAddress, endpoint, smtpUsername, smtpPassword string) EmailSender { + if !strings.Contains(endpoint, ":") { + endpoint = net.JoinHostPort(endpoint, "587") + } return &AwsSesSender{ name: name, fromEmailAddress: fromEmailAddress, smtpUsername: smtpUsername, smtpPassword: smtpPassword, + smtpEndpoint: endpoint, } } @@ -61,6 +74,13 @@ func (sender *AwsSesSender) SendEmail( } } - smtpAuth := smtp.PlainAuth("", sender.smtpUsername, sender.smtpPassword, smtpAuthAddress) - return e.Send(smtpServerAddress, smtpAuth) + host, _, err := net.SplitHostPort(sender.smtpEndpoint) + if err != nil { + return fmt.Errorf("invalid SMTP endpoint: %w", err) + } + var smtpAuth smtp.Auth + if sender.smtpUsername != "" || sender.smtpPassword != "" { + smtpAuth = smtp.PlainAuth("", sender.smtpUsername, sender.smtpPassword, host) + } + return e.Send(sender.smtpEndpoint, smtpAuth) } diff --git a/backend/mail/smtp_test.go b/backend/mail/smtp_test.go new file mode 100644 index 00000000..cb0289cb --- /dev/null +++ b/backend/mail/smtp_test.go @@ -0,0 +1,70 @@ +package mail + +import ( + "bufio" + "fmt" + "net" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestSendEmailToLocalSMTPWithoutAuthentication(t *testing.T) { + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + t.Cleanup(func() { listener.Close() }) + message := make(chan string, 1) + go func() { + conn, err := listener.Accept() + if err != nil { + return + } + defer conn.Close() + conn.SetDeadline(time.Now().Add(5 * time.Second)) + fmt.Fprint(conn, "220 localhost SMTP test\r\n") + scanner := bufio.NewScanner(conn) + var body strings.Builder + inData := false + for scanner.Scan() { + line := scanner.Text() + if inData { + if line == "." { + message <- body.String() + fmt.Fprint(conn, "250 accepted\r\n") + inData = false + } else { + body.WriteString(line + "\n") + } + continue + } + switch { + case strings.HasPrefix(line, "EHLO"), strings.HasPrefix(line, "HELO"): + fmt.Fprint(conn, "250 localhost\r\n") + case strings.HasPrefix(line, "MAIL FROM:"), strings.HasPrefix(line, "RCPT TO:"): + fmt.Fprint(conn, "250 OK\r\n") + case line == "DATA": + inData = true + fmt.Fprint(conn, "354 send message\r\n") + case line == "QUIT": + fmt.Fprint(conn, "221 bye\r\n") + return + default: + fmt.Fprint(conn, "500 unsupported command\r\n") + } + } + }() + + sender := NewSMTPSender("Talebound", "noreply@talebound.test", listener.Addr().String(), "", "") + require.NoError(t, sender.SendEmail("Local verification", "

Verify your email

", + []string{"developer@talebound.test"}, nil, nil, nil)) + select { + case body := <-message: + require.Contains(t, body, "Subject: Local verification") + require.Contains(t, body, "developer@talebound.test") + require.Contains(t, body, "Verify your email") + case <-time.After(5 * time.Second): + t.Fatal("SMTP server did not receive the message") + } +} diff --git a/backend/main.go b/backend/main.go index f613be22..6643b015 100644 --- a/backend/main.go +++ b/backend/main.go @@ -110,7 +110,7 @@ func runDBMigration(migrationURL string, dbSource string, migrationObjectsURL st } func runTaskProcessor(config util.Config, redisOpt asynq.RedisClientOpt, store db.Store) { - mailer := mail.NewAwsSesSender(config.EmailSenderName, config.EmailSenderAddress, config.SmtpUsername, config.SmtpPassword) + mailer := mail.NewSMTPSender(config.EmailSenderName, config.EmailSenderAddress, config.SmtpEndpoint, config.SmtpUsername, config.SmtpPassword) taskProcessor := worker.NewRedisTaskProcessor(redisOpt, store, mailer, config) log.Info().Msg("starting task processor") err := taskProcessor.Start() diff --git a/frontend/adapters/LocationAdapter.ts b/frontend/adapters/LocationAdapter.ts index 8dcb597a..9dd197cd 100644 --- a/frontend/adapters/LocationAdapter.ts +++ b/frontend/adapters/LocationAdapter.ts @@ -29,7 +29,10 @@ export const locationSelectors = LocationAdapter.getSelectors( export const selectLocationsByIds = createSelector( [locationSelectors.selectEntities, (_, ids: number[]) => ids], - (entities, ids) => ids.map((id) => entities[id]).filter(Boolean), + (entities, ids) => + ids + .map((id) => entities[id]) + .filter((location): location is PbLocation => location !== undefined), ); // And then use the selectors to retrieve values diff --git a/frontend/api/entities/useCreateEntityGroupContent.ts b/frontend/api/entities/useCreateEntityGroupContent.ts index 71de2b58..db540941 100644 --- a/frontend/api/entities/useCreateEntityGroupContent.ts +++ b/frontend/api/entities/useCreateEntityGroupContent.ts @@ -10,8 +10,13 @@ export type CreateEntityGroupContentParams = { }; export const useCreateEntityGroupContent = createMutation({ - mutationFn: async (variables: CreateEntityGroupContentParams) => - EntitiesCollection.entitiesCreateEntityGroupContent(variables.entityGroupId, variables.body), + mutationFn: async (variables: CreateEntityGroupContentParams) => { + if (!variables.entityGroupId) throw new Error('An entity group is required'); + return EntitiesCollection.entitiesCreateEntityGroupContent( + variables.entityGroupId, + variables.body, + ); + }, onSuccess: (data, variables) => { const entityGroupContent = data.data; if (entityGroupContent) { @@ -29,7 +34,8 @@ export const useCreateEntityGroupContent = createMutation({ ...oldData, contents: [ ...cachedData.map((c) => { - if (!c.position) return c; + if (c.position === undefined || entityGroupContent.position === undefined) + return c; let position = c.position; if ( diff --git a/frontend/api/images/useGetImages.ts b/frontend/api/images/useGetImages.ts index aac3e620..ac394581 100644 --- a/frontend/api/images/useGetImages.ts +++ b/frontend/api/images/useGetImages.ts @@ -14,8 +14,7 @@ export const useGetImages = createInfiniteQuery< primaryKey: 'useGetImages', queryFn: async ({ queryKey: [_primaryKey, variables], pageParam: offset }) => { const { data } = await ImagesCollection.imagesGetImages({ - userId: variables?.userId, - imageTypeId: variables?.imageTypeId, + ...variables, limit: PAGE_SIZE, offset, }); diff --git a/frontend/api/images/useUploadImage.ts b/frontend/api/images/useUploadImage.ts index 174259d5..d6a65146 100644 --- a/frontend/api/images/useUploadImage.ts +++ b/frontend/api/images/useUploadImage.ts @@ -1,4 +1,4 @@ -import { createMutation, inferData } from 'react-query-kit'; +import { createMutation } from 'react-query-kit'; import { ImagesCollection } from '../collections'; import { queryClient } from '../../pages/_app'; import { useGetImages } from './useGetImages'; @@ -14,69 +14,9 @@ export const useUploadImage = createMutation({ mutationFn: async (variables: ExpandedUploadImageRequest) => ImagesCollection.imagesUploadDefaultImage(variables), - onSuccess: (data, variables) => { - const queryKeys = []; + onSuccess: (data) => { store.dispatch(imageAdapterSlice.actions.upsertImage(data.data)); - - if (variables.userId && variables.imageTypeId) { - queryKeys.push( - useGetImages.getKey({ - userId: variables.userId, - imageTypeId: variables.imageTypeId, - }), - ); - } - - if (variables.userId) { - queryKeys.push( - useGetImages.getKey({ - userId: variables.userId, - }), - ); - } - - if (variables.imageTypeId) { - queryKeys.push( - useGetImages.getKey({ - imageTypeId: variables.imageTypeId, - }), - ); - } - - queryKeys.push(useGetImages.getKey({})); - - queryKeys.forEach((queryKey) => { - queryClient.setQueryData>(queryKey, (oldData) => { - const newData = oldData - ? { - pages: oldData.pages.map((page, pageIndex) => { - const images = page.images; - - if (images && pageIndex === 0) { - images.unshift(data.data); - } - - return { - ...page, - images, - totalCount: (page.totalCount ?? 0) + 1, - newOffset: page.newOffset ? page.newOffset + 1 : undefined, - }; - }), - pageParams: oldData.pageParams, - } - : { - pages: [ - { - images: [data.data], - totalCount: 1, - newOffset: undefined, - }, - ], - pageParams: [undefined], - }; - return newData; - }); - }); + // Refetch filtered pages so their membership, counts, and offsets stay consistent. + return queryClient.invalidateQueries({ queryKey: useGetImages.getKey() }); }, }); diff --git a/frontend/api/locations/useCreateLocation.ts b/frontend/api/locations/useCreateLocation.ts index f1390c0d..18b5f481 100644 --- a/frontend/api/locations/useCreateLocation.ts +++ b/frontend/api/locations/useCreateLocation.ts @@ -1,3 +1,7 @@ +import { queryClient } from '../../pages/_app'; +import { useGetLocations } from './useGetLocations'; +import { store } from '../../store'; +import { locationAdapterSlice } from '../../adapters/LocationAdapter'; import { createMutation } from 'react-query-kit'; import { LocationsCollection } from '../collections'; import { PbCreateLocationRequest } from '../../generated/api-types/data-contracts'; @@ -5,16 +9,8 @@ import { PbCreateLocationRequest } from '../../generated/api-types/data-contract export const useCreateLocation = createMutation({ mutationFn: async (variables: PbCreateLocationRequest) => LocationsCollection.locationsCreateLocation(variables), - onSuccess: (data, variables) => { - const newLocation = data.data; - if (newLocation && variables.moduleId) { - // const getLocationsQueryKey = useGetLocations.getKey(variables.module); - // queryClient.setQueryData>( - // getLocationsQueryKey, - // (locations) => { - // return [...(locations ?? []), newLocation]; - // }, - // ); - } + onSuccess: (data) => { + store.dispatch(locationAdapterSlice.actions.upsertLocation(data.data)); + return queryClient.invalidateQueries({ queryKey: useGetLocations.getKey() }); }, }); diff --git a/frontend/api/locations/useDeleteBulkLocation.ts b/frontend/api/locations/useDeleteBulkLocation.ts index 0f9ddc86..c47d5b4c 100644 --- a/frontend/api/locations/useDeleteBulkLocation.ts +++ b/frontend/api/locations/useDeleteBulkLocation.ts @@ -1,27 +1,19 @@ -import { createMutation, inferData } from 'react-query-kit'; +import { createMutation } from 'react-query-kit'; import { LocationsCollection } from '../collections'; import { queryClient } from '../../pages/_app'; -import { PbModule } from '../../generated/api-types/data-contracts'; import { useGetLocations } from './useGetLocations'; +import { store } from '../../store'; +import { locationAdapterSlice } from '../../adapters/LocationAdapter'; interface DeleteBulkLocationParams { locationIds: number[]; - module?: PbModule; } export const useDeleteBulkLocation = createMutation({ mutationFn: async (variables: DeleteBulkLocationParams) => LocationsCollection.locationsDeleteBulkLocation({ locationIds: variables.locationIds }), onSuccess: (_, variables) => { - const deletedLocationIds = variables.locationIds; - const keyParams = variables.module; - - if (deletedLocationIds && keyParams) { - const getLocationsQueryKey = useGetLocations.getKey(keyParams); - queryClient.setQueryData>( - getLocationsQueryKey, - (oldData) => oldData?.filter(({ id }) => (id ? !deletedLocationIds.includes(id) : true)), - ); - } + store.dispatch(locationAdapterSlice.actions.removeLocations(variables.locationIds)); + return queryClient.invalidateQueries({ queryKey: useGetLocations.getKey() }); }, }); diff --git a/frontend/api/locations/useDeleteLocation.ts b/frontend/api/locations/useDeleteLocation.ts index 0f5f7994..dca0dc5e 100644 --- a/frontend/api/locations/useDeleteLocation.ts +++ b/frontend/api/locations/useDeleteLocation.ts @@ -1,3 +1,7 @@ +import { queryClient } from '../../pages/_app'; +import { useGetLocations } from './useGetLocations'; +import { store } from '../../store'; +import { locationAdapterSlice } from '../../adapters/LocationAdapter'; import { createMutation } from 'react-query-kit'; import { LocationsCollection } from '../collections'; @@ -8,4 +12,8 @@ interface DeleteLocationParams { export const useDeleteLocation = createMutation({ mutationFn: async (variables: DeleteLocationParams) => LocationsCollection.locationsDeleteLocation(variables.locationId), + onSuccess: (_, variables) => { + store.dispatch(locationAdapterSlice.actions.removeLocation(variables.locationId)); + return queryClient.invalidateQueries({ queryKey: useGetLocations.getKey() }); + }, }); diff --git a/frontend/api/locations/useUpdateLocation.ts b/frontend/api/locations/useUpdateLocation.ts index d4578d1a..8e074b12 100644 --- a/frontend/api/locations/useUpdateLocation.ts +++ b/frontend/api/locations/useUpdateLocation.ts @@ -1,40 +1,20 @@ import { LocationsCollection } from '../collections'; -import { createMutation, inferData } from 'react-query-kit'; +import { createMutation } from 'react-query-kit'; import { queryClient } from '../../pages/_app'; import { useGetLocations } from './useGetLocations'; -import { PbViewModule } from '../../generated/api-types/data-contracts'; import { store } from '../../store'; import { locationAdapterSlice } from '../../adapters/LocationAdapter'; type UpdateLocationParams = { locationId: number; - module?: PbViewModule; body: Parameters[1]; }; export const useUpdateLocation = createMutation({ mutationFn: async (variables: UpdateLocationParams) => LocationsCollection.locationsUpdateLocation(variables.locationId, variables.body), - onSuccess: (data, variables) => { - const locationId = variables.locationId; - const keyParams = variables.module; - - if (locationId && keyParams) { - const getLocationsQueryKey = useGetLocations.getKey(keyParams); - queryClient.setQueryData>( - getLocationsQueryKey, - (locations) => { - return ( - locations?.map((l) => { - if (l.id === locationId) { - return data.data; - } - return l; - }) ?? [] - ); - }, - ); - store.dispatch(locationAdapterSlice.actions.upsertLocation(data.data)); - } + onSuccess: (data) => { + store.dispatch(locationAdapterSlice.actions.upsertLocation(data.data)); + return queryClient.invalidateQueries({ queryKey: useGetLocations.getKey() }); }, }); diff --git a/frontend/api/middlewareEnabled.ts b/frontend/api/middlewareEnabled.ts index c00eb3a5..9868e001 100644 --- a/frontend/api/middlewareEnabled.ts +++ b/frontend/api/middlewareEnabled.ts @@ -1,4 +1,4 @@ -import { Middleware, QueryHook, SuspenseQueryHook } from 'react-query-kit'; +import { Middleware, QueryHook } from 'react-query-kit'; // import { Middleware, QueryHook, SuspenseQueryHook } from 'react-query-kit'; // diff --git a/frontend/component-sections/Location/LocationForm.tsx b/frontend/component-sections/Location/LocationForm.tsx index 91eb11fa..b98de1eb 100644 --- a/frontend/component-sections/Location/LocationForm.tsx +++ b/frontend/component-sections/Location/LocationForm.tsx @@ -3,10 +3,11 @@ import Textarea from '../../components/Textarea/Textarea'; import { useInput } from '../../hooks/useInput'; import Input from '../../components/Input/Input'; import { Col, Row } from '../../components/Flex/Flex'; -import Avatar from '../../components/Avatar/Avatar'; +import AvatarById from '../../components/AvatarById/AvatarById'; +import { useGetModuleId } from '../../api/modules/useGetModuleId'; import { Label } from '../../components/Typography/Label'; import ImageModal from '../../components/ImageModal/ImageModal'; -import { PbImage, PbModule, PbViewLocation } from '../../generated/api-types/data-contracts'; +import { PbImage, PbViewModule, PbLocation } from '../../generated/api-types/data-contracts'; import ErrorText from '../../components/ErrorText/ErrorText'; import { Button } from '../../components/Button/Button'; import { useCreateLocation } from '../../api/locations/useCreateLocation'; @@ -16,12 +17,12 @@ const textareaPlaceholder = 'Short description of the post. What information does this post contain?'; interface LocationFormProps { - module: PbModule; + module: PbViewModule; canChangeName?: boolean; canChangeDescription?: boolean; canChangeThumbnail?: boolean; onFinishCallback?: () => void; - location?: PbViewLocation; + location?: PbLocation; } const LocationForm: React.FC = ({ @@ -32,6 +33,7 @@ const LocationForm: React.FC = ({ onFinishCallback, location, }) => { + const { data: moduleId } = useGetModuleId({ variables: module }); const { mutate: createLocation, isPending: isPendingCreate, @@ -48,9 +50,6 @@ const LocationForm: React.FC = ({ const [thumbnailImageId, setThumbnailImageId] = useState( location ? location.thumbnailImageId : undefined, ); - const [thumbnailImageUrl, setThumbnailImageUrl] = useState( - location ? location.thumbnailImageUrl : undefined, - ); const { value: name, onChange: onChangeName } = useInput( location ? location.name ?? '' : '', ); @@ -63,17 +62,16 @@ const LocationForm: React.FC = ({ }, []); const changeThumbnailImage = useCallback( - (image: PbImage) => { - if (canChangeThumbnail) { + (image: PbImage | undefined) => { + if (canChangeThumbnail && image) { setThumbnailImageId(image.id); - setThumbnailImageUrl(image.baseUrl); } }, [canChangeThumbnail], ); const createLocationHandler = useCallback(() => { - if (module !== undefined) { + if (moduleId) { const options = { onSuccess: () => { if (onFinishCallback) onFinishCallback(); @@ -92,7 +90,6 @@ const LocationForm: React.FC = ({ updateLocation( { locationId: location.id, - module, body, }, options, @@ -103,13 +100,14 @@ const LocationForm: React.FC = ({ createLocation( { ...body, + moduleId, }, options, ); } } }, [ - module, + moduleId, canChangeName, name, canChangeDescription, @@ -151,11 +149,11 @@ const LocationForm: React.FC = ({ {canChangeThumbnail && ( - )} diff --git a/frontend/component-sections/Location/LocationFormModal.tsx b/frontend/component-sections/Location/LocationFormModal.tsx index ea2c66bc..ccda79b6 100644 --- a/frontend/component-sections/Location/LocationFormModal.tsx +++ b/frontend/component-sections/Location/LocationFormModal.tsx @@ -1,11 +1,11 @@ import React, { useCallback, useMemo } from 'react'; import Modal from '../../components/Modal/Modal'; import LocationForm from './LocationForm'; -import { PbModule, PbViewLocation } from '../../generated/api-types/data-contracts'; +import { PbViewModule, PbLocation } from '../../generated/api-types/data-contracts'; interface LocationFormModalProps { - module: PbModule; - location?: PbViewLocation; + module: PbViewModule; + location?: PbLocation; trigger: React.ReactNode; open?: boolean; setOpen: (v: boolean) => void; diff --git a/frontend/component-sections/Location/LocationList.tsx b/frontend/component-sections/Location/LocationList.tsx index 3c03a902..1f107b36 100644 --- a/frontend/component-sections/Location/LocationList.tsx +++ b/frontend/component-sections/Location/LocationList.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { Button } from '../../components/Button/Button'; import { useModuleRoute } from '../../hooks/useModuleRoute'; import { useGetLocations } from '../../api/locations/useGetLocations'; import LocationTable from './LocationTable'; @@ -9,13 +10,27 @@ interface LocationListProps { } const LocationList: React.FC = ({ canEdit }) => { - const [module] = useModuleRoute(PbEntityType.ENTITY_TYPE_LOCATION); + const [module, moduleId] = useModuleRoute(PbEntityType.ENTITY_TYPE_LOCATION); - const { data: locationsData = [] } = useGetLocations({ variables: module }); + const { + data: locationsData, + hasNextPage, + fetchNextPage, + isFetchingNextPage, + } = useGetLocations({ variables: { moduleId }, enabled: !!moduleId }); return (
- + page.locationIds) ?? []} + canEdit={canEdit} + module={module} + /> + {hasNextPage && ( + + )}
); }; diff --git a/frontend/component-sections/Location/LocationListSelection.tsx b/frontend/component-sections/Location/LocationListSelection.tsx index bad6ab43..004975a2 100644 --- a/frontend/component-sections/Location/LocationListSelection.tsx +++ b/frontend/component-sections/Location/LocationListSelection.tsx @@ -1,4 +1,6 @@ import React from 'react'; +import { Button } from '../../components/Button/Button'; +import { useGetModuleId } from '../../api/modules/useGetModuleId'; import { useGetLocations } from '../../api/locations/useGetLocations'; import LocationTable from './LocationTable'; import { PbViewModule } from '../../generated/api-types/data-contracts'; @@ -9,9 +11,28 @@ interface LocationListSelectionProps { } const LocationListSelection: React.FC = ({ module }) => { - const { data: locationsData = [] } = useGetLocations({ variables: module }); + const { data: moduleId } = useGetModuleId({ variables: module }); + const { + data: locationsData, + hasNextPage, + fetchNextPage, + isFetchingNextPage, + } = useGetLocations({ variables: { moduleId }, enabled: !!moduleId }); - return ; + return ( +
+ page.locationIds) ?? []} + module={module} + isSelectionTable={true} + /> + {hasNextPage && ( + + )} +
+ ); }; export default LocationListSelection; diff --git a/frontend/component-sections/Location/LocationTable.tsx b/frontend/component-sections/Location/LocationTable.tsx index 5722e8c2..035cfcce 100644 --- a/frontend/component-sections/Location/LocationTable.tsx +++ b/frontend/component-sections/Location/LocationTable.tsx @@ -1,12 +1,14 @@ import React, { useCallback, useMemo, useState } from 'react'; import { ColumnType } from 'antd/es/table'; -import Avatar from '../../components/Avatar/Avatar'; +import AvatarById from '../../components/AvatarById/AvatarById'; +import { useAppSelector } from '../../store/hooks'; +import { selectLocationsByIds } from '../../adapters/LocationAdapter'; import { Table } from 'antd'; import { TableProps } from 'antd/lib'; import { Button } from '../../components/Button/Button'; import { MdDelete, MdEdit } from 'react-icons/md'; import LocationFormModal from './LocationFormModal'; -import { PbViewLocation, PbViewModule } from '../../generated/api-types/data-contracts'; +import { PbLocation, PbViewModule } from '../../generated/api-types/data-contracts'; import { useDeleteBulkLocation } from '../../api/locations/useDeleteBulkLocation'; import ErrorText from '../../components/ErrorText/ErrorText'; import { Row } from '../../components/Flex/Flex'; @@ -15,7 +17,7 @@ import AlertDialog from '../../components/AlertDialog/AlertDialog'; import LocationTablePostCell from './LocationTablePostCell'; interface LocationTableProps { - data: PbViewLocation[]; + locationIds: number[]; canEdit?: boolean; module: PbViewModule; isSelectionTable?: boolean; @@ -23,14 +25,15 @@ interface LocationTableProps { } const LocationTable: React.FC = ({ - data, + locationIds, canEdit, module, isSelectionTable = false, isSelectionMultiple = false, }) => { + const data = useAppSelector((state) => selectLocationsByIds(state, locationIds)); const [selectedRowKeys, setSelectedRowKeys] = useState([]); - const [updateLocation, setUpdateLocation] = useState(); + const [updateLocation, setUpdateLocation] = useState(); canEdit = canEdit && !isSelectionTable; @@ -55,7 +58,7 @@ const LocationTable: React.FC = ({ setSelectedRowKeys(newSelectedRowKeys); }, []); - const rowSelection: TableProps['rowSelection'] = useMemo( + const rowSelection: TableProps['rowSelection'] = useMemo( () => canEdit || isSelectionTable ? { @@ -67,10 +70,10 @@ const LocationTable: React.FC = ({ [canEdit, selectedRowKeys, onSelectChange, isSelectionTable, isSelectionMultiple], ); - const openEditModal = useCallback((record: PbViewLocation) => setUpdateLocation(record), []); + const openEditModal = useCallback((record: PbLocation) => setUpdateLocation(record), []); const deleteLocationRecord = useCallback( - (record: PbViewLocation) => { + (record: PbLocation) => { const { id: locationId } = record; if (locationId) { deleteLocation({ locationId }); @@ -80,7 +83,7 @@ const LocationTable: React.FC = ({ ); const actionButtons = useCallback( - (record: PbViewLocation) => { + (record: PbLocation) => { const updateHandler = () => openEditModal(record); const deleteHandler = () => deleteLocationRecord(record); @@ -106,13 +109,13 @@ const LocationTable: React.FC = ({ [openEditModal, deleteLocationRecord], ); - const columns: ColumnType[] = useMemo(() => { - const cols: ColumnType[] = [ + const columns: ColumnType[] = useMemo(() => { + const cols: ColumnType[] = [ { title: '', - key: 'thumbnailImageUrl', - dataIndex: 'thumbnailImageUrl', - render: (value: string) => , + key: 'thumbnailImageId', + dataIndex: 'thumbnailImageId', + render: (value: number | undefined) => , }, { title: 'Name', @@ -130,18 +133,16 @@ const LocationTable: React.FC = ({ }, { title: 'Post', - key: 'postTitle', - dataIndex: 'postTitle', + key: 'postId', + dataIndex: 'postId', render: (_, record) => record.id && ( ), - sorter: (a, b) => (a.postTitle ?? '').localeCompare(b.postTitle ?? ''), width: '150px', }, ]; @@ -157,14 +158,14 @@ const LocationTable: React.FC = ({ return cols; }, [canEdit, actionButtons]); - const summary: TableProps['summary'] = useCallback(() => { + const summary: TableProps['summary'] = useCallback(() => { if (!canEdit) return undefined; if (selectedRowKeys.length === 0) return undefined; const deleteBulkHandler = () => { const locationIds = selectedRowKeys.map((key) => parseInt(key.toString())); deleteBulkLocations( - { locationIds, module }, + { locationIds }, { onSuccess: () => { setSelectedRowKeys([]); @@ -197,10 +198,10 @@ const LocationTable: React.FC = ({ ); - }, [canEdit, deleteBulkLocations, errorDeleteBulk, isPendingDeleteBulk, module, selectedRowKeys]); + }, [canEdit, deleteBulkLocations, errorDeleteBulk, isPendingDeleteBulk, selectedRowKeys]); - const onRow: TableProps['onRow'] = useCallback( - (record: PbViewLocation) => { + const onRow: TableProps['onRow'] = useCallback( + (record: PbLocation) => { if (!isSelectionTable) return {}; return { @@ -214,7 +215,7 @@ const LocationTable: React.FC = ({ return ( <> - + loading={isPendingDelete} rowSelection={rowSelection} showSorterTooltip={false} diff --git a/frontend/component-sections/Location/LocationTablePostCell.tsx b/frontend/component-sections/Location/LocationTablePostCell.tsx index 313a3f79..97ffebac 100644 --- a/frontend/component-sections/Location/LocationTablePostCell.tsx +++ b/frontend/component-sections/Location/LocationTablePostCell.tsx @@ -1,3 +1,4 @@ +import { usePost } from '../../hooks/usePost'; import React, { useCallback, useMemo, useState } from 'react'; import { MdAdd } from 'react-icons/md'; import { useCreateLocationPost } from '../../api/locations/useCreateLocationPost'; @@ -8,16 +9,15 @@ import ErrorText from '../../components/ErrorText/ErrorText'; interface LocationTablePostCellProps { locationId: number; postId?: number; - postTitle?: string; canEdit?: boolean; } const LocationTablePostCell: React.FC = ({ locationId, postId, - postTitle, canEdit, }) => { + const { post } = usePost(postId); const [assignPostModal, setAssignPostModal] = useState(false); const { @@ -62,7 +62,7 @@ const LocationTablePostCell: React.FC = ({ ); } - return
{postTitle}
; + return
{post?.title}
; }; export default LocationTablePostCell; diff --git a/frontend/component-sections/Map/MapAdministrationModals/MapLayerAdministrationModal/MapLayerSectionDraggable.tsx b/frontend/component-sections/Map/MapAdministrationModals/MapLayerAdministrationModal/MapLayerSectionDraggable.tsx index d8bf0bf8..4267de52 100644 --- a/frontend/component-sections/Map/MapAdministrationModals/MapLayerAdministrationModal/MapLayerSectionDraggable.tsx +++ b/frontend/component-sections/Map/MapAdministrationModals/MapLayerAdministrationModal/MapLayerSectionDraggable.tsx @@ -1,7 +1,6 @@ import React, { useCallback, useEffect, useState } from 'react'; import { PbViewMapLayer } from '../../../../generated/api-types/data-contracts'; import { Row } from '../../../../components/Flex/Flex'; -import ContentSection from '../../../../components/ContentSection/ContentSection'; import { Reorder, useDragControls } from 'framer-motion'; import { useUpdateMapLayer } from '../../../../api/maps/useUpdateMapLayer'; import { DragHandle } from '../../../../screens/menus/MenuAdministration/menuAdministrationComponents'; @@ -32,7 +31,6 @@ const MapLayerSectionDraggable: React.FC = ({ const { mutate: updateMapLayer, isPending: isPendingUpdate, - isError: isErrorUpdate, error: errorUpdate, } = useUpdateMapLayer({ onSettled, diff --git a/frontend/component-sections/Map/MapAdministrationModals/MapPinAdministrationModal/MapPinAdministrationContent.tsx b/frontend/component-sections/Map/MapAdministrationModals/MapPinAdministrationModal/MapPinAdministrationContent.tsx index dfbe1009..807b3ecf 100644 --- a/frontend/component-sections/Map/MapAdministrationModals/MapPinAdministrationModal/MapPinAdministrationContent.tsx +++ b/frontend/component-sections/Map/MapAdministrationModals/MapPinAdministrationModal/MapPinAdministrationContent.tsx @@ -12,7 +12,7 @@ interface MapPinAdministrationContentProps { mapId: number; } -const MapPinAdministrationContent: React.FC = ({ mapId }) => { +const MapPinAdministrationContent: React.FC = () => { return ( diff --git a/frontend/component-sections/Map/MapAdministrationModals/MapPinTypeAdministrationModal/MapPinTypeAdministrationContent.tsx b/frontend/component-sections/Map/MapAdministrationModals/MapPinTypeAdministrationModal/MapPinTypeAdministrationContent.tsx index 8d40075c..0d52d55c 100644 --- a/frontend/component-sections/Map/MapAdministrationModals/MapPinTypeAdministrationModal/MapPinTypeAdministrationContent.tsx +++ b/frontend/component-sections/Map/MapAdministrationModals/MapPinTypeAdministrationModal/MapPinTypeAdministrationContent.tsx @@ -23,9 +23,7 @@ interface MapPinTypeAdministrationContentProps { mapId: number; } -const MapPinTypeAdministrationContent: React.FC = ({ - mapId, -}) => { +const MapPinTypeAdministrationContent: React.FC = () => { const [state, dispatch] = useReducer( mapPinTypeAdministrationReducer, mapPinTypeAdministrationInitialState, diff --git a/frontend/component-sections/Map/MapAdministrationModals/MapPinTypeAdministrationModal/PinTypeActions/PinTypeActions.tsx b/frontend/component-sections/Map/MapAdministrationModals/MapPinTypeAdministrationModal/PinTypeActions/PinTypeActions.tsx index f7a46e11..289c841d 100644 --- a/frontend/component-sections/Map/MapAdministrationModals/MapPinTypeAdministrationModal/PinTypeActions/PinTypeActions.tsx +++ b/frontend/component-sections/Map/MapAdministrationModals/MapPinTypeAdministrationModal/PinTypeActions/PinTypeActions.tsx @@ -1,8 +1,6 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { Col, Row } from '../../../../../components/Flex/Flex'; import { TitleH2 } from '../../../../../components/Typography/Title'; -import { useUrlModuleId } from '../../../../../hooks/useUrlModuleId'; -import { useGetMapPinTypesAndGroups } from '../../../../../api/maps/useGetMapPinTypesAndGroups'; import { Label } from '../../../../../components/Typography/Label'; import { Button } from '../../../../../components/Button/Button'; import { Text } from '../../../../../components/Typography/Text'; @@ -13,17 +11,7 @@ interface PinTypeActionsProps { pinTypeId: number; } -const PinTypeActions: React.FC = ({ pinTypeId }) => { - const moduleId = useUrlModuleId(); - const { data: mapPinTypesAndGroups, isFetching: isPending } = useGetMapPinTypesAndGroups({ - variables: moduleId, - }); - - const pinData = useMemo( - () => (mapPinTypesAndGroups?.pinTypes ?? []).find((x) => x.id === pinTypeId), - [mapPinTypesAndGroups, pinTypeId], - ); - +const PinTypeActions: React.FC = () => { return ( Actions diff --git a/frontend/component-sections/Map/MapLayout/MapLayout.tsx b/frontend/component-sections/Map/MapLayout/MapLayout.tsx index 612194f8..d44b3563 100644 --- a/frontend/component-sections/Map/MapLayout/MapLayout.tsx +++ b/frontend/component-sections/Map/MapLayout/MapLayout.tsx @@ -27,8 +27,8 @@ interface MapLayoutProps { } const MapLayout: React.FC = ({ mapId, canEdit, allLayersByDefault = false }) => { - const { map: mapData, isFetching: isPendingMap } = useMap(mapId); - const { data: mapLayers, isFetching: isPendingMapLayers } = useGetMapLayers({ variables: mapId }); + const { map: mapData } = useMap(mapId); + const { data: mapLayers } = useGetMapLayers({ variables: mapId }); const wrapperRef = useRef(null); const [displayedLayers, setDisplayedLayers] = useState({}); diff --git a/frontend/component-sections/Map/MapList.tsx b/frontend/component-sections/Map/MapList.tsx index c9d55abd..0fce4dad 100644 --- a/frontend/component-sections/Map/MapList.tsx +++ b/frontend/component-sections/Map/MapList.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { ReduxState } from '../../store'; -import { useSelector } from 'react-redux'; +import { useAppSelector } from '../../store/hooks'; import { EntityTableType, Tables } from '../../utils/types/tables'; import { PAGE_SIZE_MAPS, useGetMaps } from '../../api/maps/useGetMaps'; import { selectMapsByIds } from '../../adapters/MapAdapter'; @@ -35,7 +35,7 @@ const MapList: React.FC = ({ [mapsDataPages], ); - const mapsData = useSelector((state: ReduxState) => selectMapsByIds(state, mapIds), [mapIds]); + const mapsData = useAppSelector((state: ReduxState) => selectMapsByIds(state, mapIds)); useEffect(() => { if ( diff --git a/frontend/component-sections/Map/MapSidebar/MapSidebarPins.tsx b/frontend/component-sections/Map/MapSidebar/MapSidebarPins.tsx index d2b4e546..714ba7eb 100644 --- a/frontend/component-sections/Map/MapSidebar/MapSidebarPins.tsx +++ b/frontend/component-sections/Map/MapSidebar/MapSidebarPins.tsx @@ -16,7 +16,7 @@ interface MapSidebarPinsProps { displayedLayers: DisplayedLayers; } -const MapSidebarPins: React.FC = ({ mapId, displayedLayers, canEdit }) => { +const MapSidebarPins: React.FC = ({ mapId, canEdit }) => { const { data: mapPins, isFetching: isPendingMapPins } = useGetMapPins({ variables: mapId }); const [modalOpenPins, setModalOpenPins] = useState(false); diff --git a/frontend/component-sections/Menu/MenuCategoryPage.tsx b/frontend/component-sections/Menu/MenuCategoryPage.tsx index 36484322..0a5a7da6 100644 --- a/frontend/component-sections/Menu/MenuCategoryPage.tsx +++ b/frontend/component-sections/Menu/MenuCategoryPage.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useMemo } from 'react'; import { useRouter } from 'next/router'; import MenuCategory from '../../screens/menus/MenuCategory/MenuCategory'; -import { useDispatch } from 'react-redux'; +import { useAppDispatch } from '../../store/hooks'; import { setOpenedUrlPrefix } from '../../screens/menus/MenuCategory/menuCategorySlice'; interface MenuCategoryProps { @@ -17,7 +17,7 @@ const MenuCategoryPage: React.FC = ({ menuId, canEdit, }) => { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); const router = useRouter(); const m = router.query['menuCategory']; diff --git a/frontend/component-sections/Post/ModulePosts.tsx b/frontend/component-sections/Post/ModulePosts.tsx index db88be8d..cc51da6a 100644 --- a/frontend/component-sections/Post/ModulePosts.tsx +++ b/frontend/component-sections/Post/ModulePosts.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { isModuleCollaborator, useMyModuleRole } from '../../hooks/useModuleAdmins'; import ModuleLayout from '../../components/Layout/ModuleLayout'; -import MapsScreen from '../../screens/menus/Maps/MapsScreen'; import PostsScreen from '../../screens/menus/Posts/PostsScreen'; interface ModulePostsProps { diff --git a/frontend/component-sections/Post/PostFormOld.tsx b/frontend/component-sections/Post/PostFormOld.tsx deleted file mode 100644 index e52d420f..00000000 --- a/frontend/component-sections/Post/PostFormOld.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import React, { useCallback, useState } from 'react'; -import ContentSection from '../../components/ContentSection/ContentSection'; -import Textarea from '../../components/Textarea/Textarea'; -import { useInput } from '../../hooks/useInput'; -import Input from '../../components/Input/Input'; -import { Col, Row } from '../../components/Flex/Flex'; -import Avatar from '../../components/Avatar/Avatar'; -import { Label } from '../../components/Typography/Label'; -import ImageModal from '../../components/ImageModal/ImageModal'; -import { PbImage } from '../../generated/api-types/data-contracts'; -import ErrorText from '../../components/ErrorText/ErrorText'; -import { Button } from '../../components/Button/Button'; -import { useCreateMenuItemPost } from '../../api/menus/useCreateMenuItemPost'; - -const textareaPlaceholder = - 'Short description of the post. What information does this post contain?'; - -interface PostFormOldProps { - menuId?: number; - menuItemId?: number; - position?: number; - canChangeTitle?: boolean; - canChangeDescription?: boolean; - canChangeThumbnail?: boolean; - onFinishCallback?: () => void; -} - -const PostFormOld: React.FC = ({ - menuId, - menuItemId, - position, - canChangeTitle = true, - canChangeDescription = true, - canChangeThumbnail = true, - onFinishCallback, -}) => { - const { mutate: createMenuItemPost, isPending, error } = useCreateMenuItemPost(); - - const [showImageModal, setShowImageModal] = useState(false); - - const [thumbnailImageId, setThumbnailImageId] = useState(); - const [thumbnailImageUrl, setThumbnailImageUrl] = useState(); - const { value: title, onChange: onChangeTitle } = useInput(''); - const { value: description, onChange } = useInput(''); - - const toggleImageModal = useCallback(() => { - setShowImageModal((p) => !p); - }, []); - - const changeThumbnailImage = useCallback( - (image: PbImage) => { - if (canChangeThumbnail) { - setThumbnailImageId(image.id); - setThumbnailImageUrl(image.baseUrl); - } - }, - [canChangeThumbnail], - ); - - const createPostHandler = useCallback(() => { - if (menuId && menuItemId !== undefined) { - createMenuItemPost( - { - menuId, - menuItemId, - body: { - title: canChangeTitle ? title : undefined, - position, - shortDescription: canChangeDescription ? description : undefined, - imageThumbnailId: thumbnailImageId, - }, - }, - { - onSuccess: () => { - if (onFinishCallback) onFinishCallback(); - }, - }, - ); - } - }, [ - createMenuItemPost, - menuId, - menuItemId, - position, - canChangeTitle, - title, - canChangeDescription, - description, - thumbnailImageId, - ]); - - const pending = isPending; - - if (!canChangeTitle && !canChangeDescription && !canChangeThumbnail) return null; - - return ( - <> - - - {canChangeTitle && ( - - )} - {canChangeDescription && ( -