@lokalise/harmony
v4.7.3
Published
Archived: Harmony is deprecated and no longer maintained.
Downloads
249
Maintainers
Keywords
Readme
Archived: Harmony is deprecated and no longer maintained. The repository is read-only and receives no further updates.
Harmony
A temporary shared library designed to house reusable components, such as molecules and organisms, exclusively for the Expert and Flow platforms during the migration period. This library will be retired upon the completion of the migration and the establishment of the unified next-gen platform.
Deprecation Status
- Final release:
4.7.3(documentation-only). - The npm package is deprecated and will warn on install.
- Existing consumers should migrate to the platform-specific implementations that now live in the Expert, Flow, or next-gen repositories.
Storybook
https://lokalise.github.io/harmony/?path=/story/app-shell-navigationpanel--default&args=sticky:!true
Features
Authentication
This provides a wretch addon for authentication. It is used to add the authentication token to the request headers, and will handle automatic token refreshes.
To compliment this, there are a number of convenience hooks and functions to help with authentication in both a frontend and backend context.
Basic Frontend Usage
import { HeaderBuilder, JwtAuthHeaderBuilderMiddleware, generateTokenFromClassicSession, refreshExpiredToken, getJwtTokenFromCookie } from '@lokalise/harmony'
import wretch from 'wretch'
// Create a client pointing at the Lokalise Auth API
const authHttpClient = wretch('path/to/auth/provider')
// This is the client that you would use to make requests to your app's API
// - It assumes that your API is compatible with the shared authentication strategy
export const appHttpClient = wretch('path/to/you/api')
.errorType('json') // More config as you see fit
const getTeamId = async () => 000 /* Get the team ID from somewhere*/
export const authenticationHeaderBuilder = HeaderBuilder.create()
.with(
JwtAuthHeaderBuilderMiddleware({
// Provide a way to refresh the token - there are utility functions to help with this in the frontend
refreshToken: refreshExpiredToken(authHttpClient, getTeamId),
// Setup a source for the current token - there are utility functions to help with this in the frontend
getCurrentToken: getJwtTokenFromCookie,
// Additionally, you can promote a classic session to an authenticated session
// when configuring a client in the context of a classic PHP CSRF session.
// this `promoteClassicSession` method will automatically promote the session to JWT
generateNewToken: generateTokenFromClassicSession(authHttpClient, getCsrfTokenFromCookie, getTeamId),
// Optionally, you can provide a callback to be notified when a new token is issued
onNewTokenIssued: (token) => {
// Do something with the new token
}
})
)
const result = await sendByGetRoute(appHttpClient, getTeamUser, {
headers: await authenticationHeaderBuilder.resolve(),
pathParams: { teamId, userId }
})Public API
This provides a set of type safe request routes for the public API. They are designed to be used with the sendByRoute style functions that accept a wretch client and a route object.
Additionally, there are a number of convenience hooks and functions to help with public API requests.
Basic Frontend Usage
import { sendByGetRoute } from '@lokalise/frontend-http-client'
import { type TeamUserResponse, getTeamUser } from '@lokalise/harmony';
import { appAuthenticatedHttpClient } from '../path/to/appAuthenticatedHttpClient';
const result: TeamUserResponse = await sendByGetRoute(appAuthenticatedHttpClient, getTeamUser, { pathParams: { teamId, userId } });Basic Backend Usage
import { sendByGetRoute } from '@lokalise/backend-http-client'
import { type TeamUserResponse, getTeamUser } from '@lokalise/harmony';
import { Client } from 'undici'
const result: TeamUserResponse = async (client: Client) => {
return sendByGetRoute(client, getTeamUser, {
pathParams: { teamId, userId },
headers: { 'authorization': `Bearer token` }
});
}Tests
Query Hooks testing
Query hooks tests functionality has been developed due to inconsistencies in data structure from API responses and zod schemas. zod.parse will error if data is invalid and although this solution is not ideal (until we have data layer that is driving schemas for all apps), we need to do it this way.
It is important not to generate fake data but to use some actual payloads from API, as this iss the source of the problem (like date formatting for example).
If your hook is relatively simple, you may not need to add the hook tests as such, but simply add payload parsing test.
In order to test query hooks, utilise @tests/utils/apiHelpers. Setup is already extended with msw server setup, so there's no need to add any logic. Simply use normal it function, within witch you can use mockGetResponse (POST yet to be implemented) like in example below:
import {
mockGetResponse,
renderQueryHook
} from "@tests/utils/apiHelpers";
// Inside tests
// Create expected response
const response = {
user: {
id: 1,
name: 'Test User'
}
}
// Mock the response
mockGetResponse('/me', responseData)
// Use helper method renderQueryHook to utilise custom client and header builder
const { result } = renderQueryHook(({ client, builder}) => useListTeamsQuery(client, builder, {
queryKey: ['me']
}))
// Create necessary assertions
await waitFor(() => {
const { data , isSuccess} = result.current
expect(isSuccess).toBeTruthy();
expect(data).toEqual(responseData);
});Permissions Feature
The permissions feature provides a flexible and type-safe way to control access to features and actions throughout the application. It leverages context providers, guard components, and hooks to make permission checks declarative and easy to use in your React components.
Usage Examples
Providing Permissions Context
import {
FeatureFlagActionResolverContextProvider,
TeamActionResolverContextProvider,
ProjectActionResolverContextProvider
} from "@lokalise/harmony";
{/* NB :: as these are contexts and will cause everything below to re-render any time the value updates... */}
{/* we should make sure that the information is provided once and changed as little as possible. */}
{/* Ideally this is a request when the page loads and then doesn't change until absolutly needed. */}
{/* Provide the FeatureFlag once at the top level of your app */}
<FeatureFlagActionResolverContextProvider enabledFeatureFlags={["homeMarketingBeta"]}>
{/* Team level context should be provided as soon as team context becomes available */}
{/* NOTE: This might be lower in the DOM tree than feature-flags context*/}
{/* the reason they are separate is to allower for checks outside the scope of a team */}
<TeamActionResolverContextProvider teamRole="admin">
{/* Project level context should be provided when projcet information available */}
{/* NOTE: This is likely going to be lower in the DOM tree than the team context */}
<ProjectActionResolverContextProvider projectPermissions={["tasks", "upload"]} projectType="marketing">
{/* ...your app... */}
</ProjectActionResolverContextProvider>
</TeamActionResolverContextProvider>
</FeatureFlagActionResolverContextProvider>Guarding UI with Permission Checks
import { CanPerformActionGuard, CanPerformAllActionsGuard, CanPerformAnyActionGuard } from "@lokalise/harmony";
<CanPerformActionGuard action="accessMarketingProjects">
<MarketingDashboard />
</CanPerformActionGuard>
<CanPerformAllActionsGuard actions={["accessMarketingProjects", "deleteProjects"]}>
<DangerZone />
</CanPerformAllActionsGuard>
<CanPerformAnyActionGuard actions={["accessMarketingProjects", "deleteProjects"]}>
<ProjectDetails />
</CanPerformAnyActionGuard>Checking Permissions in Logic
import { useCanPerformAction, useCanPerformAllActions, useCanPerformAnyAction } from "@lokalise/harmony";
const canAccess = useCanPerformAction("accessMarketingProjects");
const canDoAll = useCanPerformAllActions("accessMarketingProjects", "deleteProjects");
const canDoAny = useCanPerformAnyAction("accessMarketingProjects", "deleteProjects");
if (canAccess) {
// show marketing dashboard
}
if (canDoAll) {
// show danger zone
}
if (canDoAny) {
// show details or enable actions
}Contributing: Creating a New Permission Action
To add a new permission action to the system, follow these steps:
Create the Action Resolver
- Implement a new resolver function in
src/features/permissions/action-resolvers/. - The resolver should accept an
ActionResolverPayloadand return a boolean indicating if the action is permitted. - Example:
// src/features/permissions/action-resolvers/canManageWidgets.ts import type { ActionResolver } from '../types'; import { has } from '../utils/has' export const canManageWidgets: ActionResolver = ({ teamRole, projectPermissions }) => { return has(teamRole, 'admin') && has(projectPermissions, 'settings'); };
- Implement a new resolver function in
Register the Action
- Add your new resolver to the
actionResolversobject insrc/features/permissions/action.ts:import { canManageWidgets } from './action-resolvers/canManageWidgets'; export const actionResolvers = { // ...existing actions... canManageWidgets, };
- Add your new resolver to the
Success!
- Your new action is now available for use throughout the application via the guards, hooks, and context providers.
- You can now use it like:
<CanPerformActionGuard action="canManageWidgets"> <WidgetAdminPanel /> </CanPerformActionGuard>
For more details, see the documentation in the src/features/permissions directory.
