Feature flags (#2961)
* [Feature Flags] Abstraction over flags provider (#2928) * Remove useFlag hook * [Feature Flags] GraphQL build multiple schemas (#2937) * Build script * Refactor build types script * Remove old codegen.yml * Clean feature flags in script * Refactor schema path * Restore useAuthProvider * Update configuration file * encapsulate details for feature flags provider * Add proper env to flagsmith provider * Remove flagsmith mocks * Vite config define global variables * Render flagmisth provider only when is used * Keep name service agnostic * Test with mocked flagsmith * Use global FLAGS varaible for env flags * Fix type issue with FLAGS * Fix build issue * Remove duplicate translations * Fix typo * Prepare for QA tests * Remove test feature flag
This commit is contained in:
parent
7a43421af5
commit
d5ed6fb202
62 changed files with 755 additions and 147 deletions
53
codegen.yml
53
codegen.yml
|
@ -1,53 +0,0 @@
|
|||
overwrite: true
|
||||
schema: ./introspection.json
|
||||
generates:
|
||||
./src/graphql/fragmentTypes.generated.ts:
|
||||
plugins:
|
||||
- add:
|
||||
content: /* eslint-disable */
|
||||
- fragment-matcher
|
||||
config:
|
||||
minify: false
|
||||
apolloClientVersion: 3
|
||||
./src/graphql/typePolicies.generated.ts:
|
||||
plugins:
|
||||
- add:
|
||||
content: /* eslint-disable */
|
||||
- typescript-apollo-client-helpers
|
||||
./src/graphql/types.generated.ts:
|
||||
documents:
|
||||
- ./src/**/queries.ts
|
||||
- ./src/**/mutations.ts
|
||||
- ./src/**/fragments/*.ts
|
||||
- ./src/searches/*.ts
|
||||
config:
|
||||
nonOptionalTypename: true
|
||||
avoidOptionals:
|
||||
field: true
|
||||
inputValue: false
|
||||
object: false
|
||||
defaultValue: false
|
||||
namingConvention:
|
||||
enumValues: change-case-all#upperCase
|
||||
onlyOperationTypes: true
|
||||
plugins:
|
||||
- add:
|
||||
content: /* eslint-disable */
|
||||
- typescript
|
||||
- typescript-operations
|
||||
./src/graphql/hooks.generated.ts:
|
||||
documents:
|
||||
- ./src/**/queries.ts
|
||||
- ./src/**/mutations.ts
|
||||
- ./src/**/fragments/*.ts
|
||||
- ./src/searches/*.ts
|
||||
preset: import-types
|
||||
presetConfig:
|
||||
typesPath: ./types.generated
|
||||
config:
|
||||
withHooks: true
|
||||
apolloReactHooksImportFrom: "@dashboard/hooks/graphql"
|
||||
plugins:
|
||||
- add:
|
||||
content: /* eslint-disable */
|
||||
- typescript-react-apollo
|
|
@ -85,8 +85,16 @@ describe("As a staff user I want to manage apps", () => {
|
|||
});
|
||||
})
|
||||
.then(({ variantsList: variants }) => {
|
||||
variantsList = variants
|
||||
cy.checkIfDataAreNotNull({createdApp,defaultChannel,address,warehouse,shippingMethod,variantsList,checkout})
|
||||
variantsList = variants;
|
||||
cy.checkIfDataAreNotNull({
|
||||
createdApp,
|
||||
defaultChannel,
|
||||
address,
|
||||
warehouse,
|
||||
shippingMethod,
|
||||
variantsList,
|
||||
checkout,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -71,8 +71,14 @@ describe("As an admin I want to manage collections.", () => {
|
|||
},
|
||||
)
|
||||
.then(({ product: productResp }) => {
|
||||
product = productResp
|
||||
cy.checkIfDataAreNotNull({attribute, category, productType, product, defaultChannel})
|
||||
product = productResp;
|
||||
cy.checkIfDataAreNotNull({
|
||||
attribute,
|
||||
category,
|
||||
productType,
|
||||
product,
|
||||
defaultChannel,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -79,7 +79,15 @@ describe("As a customer I should be able to purchase gift card as a product", ()
|
|||
})
|
||||
.then(({ variantsList: variantsResp }) => {
|
||||
variants = variantsResp;
|
||||
cy.checkIfDataAreNotNull({defaultChannel, productType, attribute, category, shippingMethod, variants, address})
|
||||
cy.checkIfDataAreNotNull({
|
||||
defaultChannel,
|
||||
productType,
|
||||
attribute,
|
||||
category,
|
||||
shippingMethod,
|
||||
variants,
|
||||
address,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -83,7 +83,14 @@ describe("Warehouses in checkout", () => {
|
|||
})
|
||||
.then(({ variantsList }) => {
|
||||
variantsInOtherWarehouse = variantsList;
|
||||
cy.checkIfDataAreNotNull({defaultChannel, usAddress, secondUsAddress, productData, checkoutData, variantsInOtherWarehouse})
|
||||
cy.checkIfDataAreNotNull({
|
||||
defaultChannel,
|
||||
usAddress,
|
||||
secondUsAddress,
|
||||
productData,
|
||||
checkoutData,
|
||||
variantsInOtherWarehouse,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -83,12 +83,17 @@ describe("Products without shipment option", () => {
|
|||
name: nameProdWithoutShipping,
|
||||
productTypeId: productTypeResp.id,
|
||||
warehouseId: warehouse.id,
|
||||
}).then(
|
||||
({ variantsList }) => {
|
||||
productWithoutShipping = variantsList
|
||||
cy.checkIfDataAreNotNull({channel, address, warehouse, shippingMethod, productWithShipping, productWithoutShipping})
|
||||
}
|
||||
);
|
||||
}).then(({ variantsList }) => {
|
||||
productWithoutShipping = variantsList;
|
||||
cy.checkIfDataAreNotNull({
|
||||
channel,
|
||||
address,
|
||||
warehouse,
|
||||
shippingMethod,
|
||||
productWithShipping,
|
||||
productWithoutShipping,
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
@ -56,7 +56,14 @@ describe("Manage products stocks in checkout", () => {
|
|||
lastVariantInStock = resp.createdVariants.find(
|
||||
variant => variant.name === "lastVariantInStock",
|
||||
);
|
||||
cy.checkIfDataAreNotNull({defaultChannel, address,shippingMethod, variantsWithLowStock, variantsWithoutTrackInventory, lastVariantInStock})
|
||||
cy.checkIfDataAreNotNull({
|
||||
defaultChannel,
|
||||
address,
|
||||
shippingMethod,
|
||||
variantsWithLowStock,
|
||||
variantsWithoutTrackInventory,
|
||||
lastVariantInStock,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ describe("As an admin I want to use attributes in variant selection", () => {
|
|||
getDefaultChannel().then(defaultChannel => (channel = defaultChannel));
|
||||
createCategory({ name: startsWith }).then(
|
||||
categoryResp => (category = categoryResp),
|
||||
cy.checkIfDataAreNotNull({channel, category})
|
||||
cy.checkIfDataAreNotNull({ channel, category }),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ describe("As an admin I want to delete and update content attribute", () => {
|
|||
}).then(attributeResp => {
|
||||
attribute = attributeResp;
|
||||
});
|
||||
cy.checkIfDataAreNotNull(attribute)
|
||||
cy.checkIfDataAreNotNull(attribute);
|
||||
});
|
||||
|
||||
it(
|
||||
|
|
|
@ -24,12 +24,10 @@ describe("Tests for menu navigation", () => {
|
|||
before(() => {
|
||||
cy.clearSessionData().loginUserViaRequest();
|
||||
deleteMenusStartsWith(startsWith);
|
||||
createMenuViaApi(randomName).then(
|
||||
({ menu: menuResp }) => {
|
||||
menu = menuResp
|
||||
cy.checkIfDataAreNotNull(menu)
|
||||
},
|
||||
);
|
||||
createMenuViaApi(randomName).then(({ menu: menuResp }) => {
|
||||
menu = menuResp;
|
||||
cy.checkIfDataAreNotNull(menu);
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -92,8 +92,17 @@ describe("Adyen payments", () => {
|
|||
});
|
||||
})
|
||||
.then(({ variantsList: variants }) => {
|
||||
variantsList = variants
|
||||
cy.checkIfDataAreNotNull({address,defaultChannel,warehouse,shippingMethod,variantsList,checkout,paymentCards,cardData})
|
||||
variantsList = variants;
|
||||
cy.checkIfDataAreNotNull({
|
||||
address,
|
||||
defaultChannel,
|
||||
warehouse,
|
||||
shippingMethod,
|
||||
variantsList,
|
||||
checkout,
|
||||
paymentCards,
|
||||
cardData,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -50,7 +50,15 @@ describe("Stripe payments", () => {
|
|||
defaultChannel = values.defaultChannel;
|
||||
shippingMethod = values.shippingMethod;
|
||||
variantsList = values.variantsList;
|
||||
cy.checkIfDataAreNotNull({address,defaultChannel,shippingMethod,variantsList,checkout,paymentCards,cardData})
|
||||
cy.checkIfDataAreNotNull({
|
||||
address,
|
||||
defaultChannel,
|
||||
shippingMethod,
|
||||
variantsList,
|
||||
checkout,
|
||||
paymentCards,
|
||||
cardData,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -30,8 +30,8 @@ describe("As an admin I want to manage product types", () => {
|
|||
createAttribute({ name: startsWith }).then(resp => (attribute = resp));
|
||||
createCategory({ name: startsWith }).then(resp => (category = resp));
|
||||
getDefaultChannel().then(resp => {
|
||||
channel = resp
|
||||
cy.checkIfDataAreNotNull({attribute, channel, category})
|
||||
channel = resp;
|
||||
cy.checkIfDataAreNotNull({ attribute, channel, category });
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -95,7 +95,14 @@ describe("As a staff user I want to create shipping zone and rate", () => {
|
|||
})
|
||||
.then(variantsListResp => {
|
||||
secondVariantsList = variantsListResp;
|
||||
cy.checkIfDataAreNotNull({defaultChannel,address,warehouse,variantsList, secondVariantsList, attribute})
|
||||
cy.checkIfDataAreNotNull({
|
||||
defaultChannel,
|
||||
address,
|
||||
warehouse,
|
||||
variantsList,
|
||||
secondVariantsList,
|
||||
attribute,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -50,7 +50,13 @@ describe("As a user I should be able to update and delete shipping method", () =
|
|||
createShippingZone(name, "US", defaultChannel.id, warehouse.id).then(
|
||||
shippingZoneResp => {
|
||||
shippingZone = shippingZoneResp;
|
||||
cy.checkIfDataAreNotNull({defaultChannel, shippingZone, shippingMethod, warehouse, usAddress})
|
||||
cy.checkIfDataAreNotNull({
|
||||
defaultChannel,
|
||||
shippingZone,
|
||||
shippingMethod,
|
||||
warehouse,
|
||||
usAddress,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
@ -40,13 +40,17 @@ describe("As a user I should be able to update and delete shipping zone", () =>
|
|||
warehouse = warehouseResp;
|
||||
|
||||
updateChannelWarehouses(defaultChannel.id, warehouse.id);
|
||||
cy.checkIfDataAreNotNull({defaultChannel, shippingZone, plAddress, warehouse})
|
||||
cy.checkIfDataAreNotNull({
|
||||
defaultChannel,
|
||||
shippingZone,
|
||||
plAddress,
|
||||
warehouse,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
cy.clearSessionData().loginUserViaRequest();
|
||||
createShippingZone(name, "US", defaultChannel.id, warehouse.id).then(
|
||||
shippingZoneResp => {
|
||||
|
|
|
@ -83,7 +83,14 @@ describe("As a user I want to create shipping method with postal codes", () => {
|
|||
})
|
||||
.then(({ variantsList: variantsListResp }) => {
|
||||
variantsList = variantsListResp;
|
||||
cy.checkIfDataAreNotNull({defaultChannel,usAddress,secondUsAddress,shippingZone,warehouse,variantsList})
|
||||
cy.checkIfDataAreNotNull({
|
||||
defaultChannel,
|
||||
usAddress,
|
||||
secondUsAddress,
|
||||
shippingZone,
|
||||
warehouse,
|
||||
variantsList,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -74,7 +74,13 @@ describe("As a staff user I want to manage shipping weights", () => {
|
|||
})
|
||||
.then(({ variantsList: variantsListResp }) => {
|
||||
variantsList = variantsListResp;
|
||||
cy.checkIfDataAreNotNull({defaultChannel,usAddress,shippingZone,warehouse,variantsList})
|
||||
cy.checkIfDataAreNotNull({
|
||||
defaultChannel,
|
||||
usAddress,
|
||||
shippingZone,
|
||||
warehouse,
|
||||
variantsList,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -51,7 +51,12 @@ describe("As a staff user I want to change shop default weight unit", () => {
|
|||
})
|
||||
.then(shippingZoneResp => {
|
||||
shippingZone = shippingZoneResp;
|
||||
cy.checkIfDataAreNotNull({defaultChannel,usAddress,shippingZone,warehouse})
|
||||
cy.checkIfDataAreNotNull({
|
||||
defaultChannel,
|
||||
usAddress,
|
||||
shippingZone,
|
||||
warehouse,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ describe("Tests for site settings", () => {
|
|||
cy.fixture("addresses").then(({ usAddress, plAddress }) => {
|
||||
address = usAddress;
|
||||
updateShopAddress(plAddress);
|
||||
cy.checkIfDataAreNotNull(address)
|
||||
cy.checkIfDataAreNotNull(address);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ describe("As an admin I want to manage warehouses", () => {
|
|||
cy.fixture("addresses").then(addresses => {
|
||||
usAddress = addresses.usAddress;
|
||||
secondUsAddress = addresses.secondUsAddress;
|
||||
cy.checkIfDataAreNotNull({usAddress,secondUsAddress})
|
||||
cy.checkIfDataAreNotNull({ usAddress, secondUsAddress });
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ describe("Tests for customer registration", () => {
|
|||
deleteCustomersStartsWith(startsWith);
|
||||
getDefaultChannel().then(channel => {
|
||||
defaultChannel = channel;
|
||||
cy.checkIfDataAreNotNull({defaultChannel})
|
||||
cy.checkIfDataAreNotNull({ defaultChannel });
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -69,7 +69,13 @@ describe("As an admin I want to create sale for products", () => {
|
|||
})
|
||||
.then(({ warehouse: warehouseResp }) => {
|
||||
warehouse = warehouseResp;
|
||||
cy.checkIfDataAreNotNull({productType,attribute,category,defaultChannel,warehouse})
|
||||
cy.checkIfDataAreNotNull({
|
||||
productType,
|
||||
attribute,
|
||||
category,
|
||||
defaultChannel,
|
||||
warehouse,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -71,7 +71,12 @@ describe("Sales discounts for variant", () => {
|
|||
warehouseId: warehouse.id,
|
||||
price: productPrice,
|
||||
};
|
||||
cy.checkIfDataAreNotNull({productData, defaultChannel, warehouse, address})
|
||||
cy.checkIfDataAreNotNull({
|
||||
productData,
|
||||
defaultChannel,
|
||||
warehouse,
|
||||
address,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -82,7 +82,14 @@ describe("As an admin I want to update sales", () => {
|
|||
warehouseId: warehouse.id,
|
||||
price: productPrice,
|
||||
};
|
||||
cy.checkIfDataAreNotNull({productData, defaultChannel, warehouse, address, sale, variants})
|
||||
cy.checkIfDataAreNotNull({
|
||||
productData,
|
||||
defaultChannel,
|
||||
warehouse,
|
||||
address,
|
||||
sale,
|
||||
variants,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -53,7 +53,11 @@ describe("As an admin I want to create voucher", () => {
|
|||
auth: "token",
|
||||
};
|
||||
},
|
||||
cy.checkIfDataAreNotNull({createdChannel, dataForCheckout, defaultChannel})
|
||||
cy.checkIfDataAreNotNull({
|
||||
createdChannel,
|
||||
dataForCheckout,
|
||||
defaultChannel,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ describe("As an admin I want to create voucher", () => {
|
|||
shippingMethodName: shippingMethodResp.name,
|
||||
auth: "token",
|
||||
};
|
||||
cy.checkIfDataAreNotNull({dataForCheckout, defaultChannel})
|
||||
cy.checkIfDataAreNotNull({ dataForCheckout, defaultChannel });
|
||||
cy.clearSessionData();
|
||||
},
|
||||
);
|
||||
|
|
|
@ -51,7 +51,7 @@ describe("As an admin I want to update vouchers", () => {
|
|||
auth: "token",
|
||||
};
|
||||
},
|
||||
cy.checkIfDataAreNotNull({dataForCheckout, defaultChannel, product})
|
||||
cy.checkIfDataAreNotNull({ dataForCheckout, defaultChannel, product }),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ describe("Test for metadata", () => {
|
|||
})
|
||||
.then(({ product: productResp }) => {
|
||||
product = productResp;
|
||||
cy.checkIfDataAreNotNull({channel, product})
|
||||
cy.checkIfDataAreNotNull({ channel, product });
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -67,7 +67,15 @@ describe("As an admin I want to manage stock reservation", () => {
|
|||
productType = productTypeResp;
|
||||
},
|
||||
);
|
||||
cy.checkIfDataAreNotNull({defaultChannel,address,warehouse,attribute,category,productType,dataForCheckout})
|
||||
cy.checkIfDataAreNotNull({
|
||||
defaultChannel,
|
||||
address,
|
||||
warehouse,
|
||||
attribute,
|
||||
category,
|
||||
productType,
|
||||
dataForCheckout,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -105,7 +105,14 @@ describe("Orders", () => {
|
|||
)
|
||||
.then(({ variantsList: variantsResp }) => {
|
||||
variantsList = variantsResp;
|
||||
cy.checkIfDataAreNotNull({customer,defaultChannel,warehouse,shippingMethod,variantsList,address})
|
||||
cy.checkIfDataAreNotNull({
|
||||
customer,
|
||||
defaultChannel,
|
||||
warehouse,
|
||||
shippingMethod,
|
||||
variantsList,
|
||||
address,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@ describe("As an admin I should be able to create product", () => {
|
|||
cy.clearSessionData().loginUserViaRequest();
|
||||
createAttribute({ name }).then(attributeResp => {
|
||||
attribute = attributeResp;
|
||||
cy.checkIfDataAreNotNull({attribute})
|
||||
cy.checkIfDataAreNotNull({ attribute });
|
||||
});
|
||||
});
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -27,7 +27,7 @@ describe("As an admin I should be able to sort products", () => {
|
|||
cy.clearSessionData().loginUserViaRequest();
|
||||
getDefaultChannel().then(channel => {
|
||||
defaultChannel = channel;
|
||||
cy.checkIfDataAreNotNull({defaultChannel})
|
||||
cy.checkIfDataAreNotNull({ defaultChannel });
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -49,8 +49,15 @@ describe("As an admin I should be able to create variant", () => {
|
|||
createChannel({ isActive: true, name, currencyCode: "PLN" });
|
||||
})
|
||||
.then(resp => {
|
||||
newChannel = resp
|
||||
cy.checkIfDataAreNotNull({defaultChannel,warehouse,attribute,productType,category,newChannel})
|
||||
newChannel = resp;
|
||||
cy.checkIfDataAreNotNull({
|
||||
defaultChannel,
|
||||
warehouse,
|
||||
attribute,
|
||||
productType,
|
||||
category,
|
||||
newChannel,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ describe("Staff members", () => {
|
|||
cy.clearSessionData().visit(urlLink);
|
||||
fillUpSetPassword(password);
|
||||
cy.clearSessionData();
|
||||
cy.checkIfDataAreNotNull({user})
|
||||
cy.checkIfDataAreNotNull({ user });
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -20,12 +20,10 @@ describe("As an admin I want to manage translations", () => {
|
|||
before(() => {
|
||||
cy.clearSessionData().loginUserViaRequest();
|
||||
deleteCategoriesStartsWith(startsWith);
|
||||
createCategory({ name: startsWith }).then(
|
||||
categoryResp => {
|
||||
category = categoryResp
|
||||
cy.checkIfDataAreNotNull({category})
|
||||
},
|
||||
);
|
||||
createCategory({ name: startsWith }).then(categoryResp => {
|
||||
category = categoryResp;
|
||||
cy.checkIfDataAreNotNull({ category });
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
Cypress.Commands.add("getTextFromElement", element =>
|
||||
cy.get(element).invoke("text")
|
||||
cy.get(element).invoke("text"),
|
||||
);
|
||||
|
||||
Cypress.Commands.add("clearAndType", { prevSubject: true }, (subject, text) => {
|
||||
|
@ -12,22 +12,22 @@ Cypress.Commands.add("waitForRequestAndCheckIfNoErrors", alias => {
|
|||
cy.wait(alias).then(resp => {
|
||||
expect(
|
||||
resp.response.body.errors,
|
||||
`No errors in ${alias} operation in graphql response`
|
||||
`No errors in ${alias} operation in graphql response`,
|
||||
).to.be.undefined;
|
||||
return resp;
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add("checkIfDataAreNotNull", (data) => {
|
||||
Cypress.Commands.add("checkIfDataAreNotNull", data => {
|
||||
expect(data, "Created data should not be null").to.be.not.null;
|
||||
if (typeof data === "object") {
|
||||
Object.keys(data).forEach(key => {
|
||||
cy.checkIfDataAreNotNull(data[key])
|
||||
})
|
||||
cy.checkIfDataAreNotNull(data[key]);
|
||||
});
|
||||
} else if (Array.isArray(data)) {
|
||||
expect(data).not.to.be.empty;
|
||||
data.forEach(singleData => {
|
||||
cy.checkIfDataAreNotNull(singleData)
|
||||
})
|
||||
cy.checkIfDataAreNotNull(singleData);
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
|
|
@ -23,3 +23,9 @@ Create `.env` file in a root directory or set environment variables with the fol
|
|||
- `APPS_MARKETPLACE_API_URI` - URI of Marketplace API to fetch list of Apps in JSON.
|
||||
|
||||
- `APPS_TUNNEL_URL_KEYWORDS` - Custom apps tunnel URL keywords.
|
||||
|
||||
- `FLAGS_SERVICE_ENABLED` - Boolean flag determines whether we use external feature flags provider.
|
||||
If you set `FLAGS_SERVICE_ENABLED` to "true", we'll be using external feature flag provider as source or flags.
|
||||
If you set `FLAGS_SERVICE_ENABLED` to "false" or not set, we'll use fallback flags from environment variables.
|
||||
|
||||
- `FLAGSMITH_ID` - Flagsmith environment id
|
||||
|
|
11
package-lock.json
generated
11
package-lock.json
generated
|
@ -40,6 +40,7 @@
|
|||
"faker": "^5.1.0",
|
||||
"fast-array-diff": "^0.2.0",
|
||||
"find-test-names": "^1.17.1",
|
||||
"flagsmith": "^3.15.1",
|
||||
"fuse.js": "^6.6.2",
|
||||
"fuzzaldrin": "^2.1.0",
|
||||
"graphql": "^15.4.0",
|
||||
|
@ -21046,6 +21047,11 @@
|
|||
"resolved": "https://registry.npmjs.org/first-match/-/first-match-0.0.1.tgz",
|
||||
"integrity": "sha1-pg7GQnAPD0NyNOu37D84JHblQv0="
|
||||
},
|
||||
"node_modules/flagsmith": {
|
||||
"version": "3.15.1",
|
||||
"resolved": "https://registry.npmjs.org/flagsmith/-/flagsmith-3.15.1.tgz",
|
||||
"integrity": "sha512-4OHVCCYuW1doYceEJbrQWJgefwRaC9oi2VeswVzCqCe0WCoT0pOIxgi14l3Am0a0fcN9wUd93Dv2OQaF6GqKjA=="
|
||||
},
|
||||
"node_modules/flat": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
|
||||
|
@ -59661,6 +59667,11 @@
|
|||
"resolved": "https://registry.npmjs.org/first-match/-/first-match-0.0.1.tgz",
|
||||
"integrity": "sha1-pg7GQnAPD0NyNOu37D84JHblQv0="
|
||||
},
|
||||
"flagsmith": {
|
||||
"version": "3.15.1",
|
||||
"resolved": "https://registry.npmjs.org/flagsmith/-/flagsmith-3.15.1.tgz",
|
||||
"integrity": "sha512-4OHVCCYuW1doYceEJbrQWJgefwRaC9oi2VeswVzCqCe0WCoT0pOIxgi14l3Am0a0fcN9wUd93Dv2OQaF6GqKjA=="
|
||||
},
|
||||
"flat": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
|
||||
|
|
|
@ -47,6 +47,7 @@
|
|||
"faker": "^5.1.0",
|
||||
"fast-array-diff": "^0.2.0",
|
||||
"find-test-names": "^1.17.1",
|
||||
"flagsmith": "^3.15.1",
|
||||
"fuse.js": "^6.6.2",
|
||||
"fuzzaldrin": "^2.1.0",
|
||||
"graphql": "^15.4.0",
|
||||
|
@ -212,6 +213,10 @@
|
|||
},
|
||||
"jest": {
|
||||
"resetMocks": false,
|
||||
"globals": {
|
||||
"FLAGS_SERVICE_ENABLED": false,
|
||||
"FLAGS": {}
|
||||
},
|
||||
"setupFilesAfterEnv": [
|
||||
"jest-canvas-mock",
|
||||
"jest-localstorage-mock",
|
||||
|
@ -262,7 +267,7 @@
|
|||
"build": "cross-env NODE_OPTIONS=--max_old_space_size=16384 vite build",
|
||||
"preview": "vite preview",
|
||||
"build-storybook": "cross-env NODE_OPTIONS=--openssl-legacy-provider build-storybook -c src/storybook/ -o build/storybook",
|
||||
"build-types": "graphql-codegen",
|
||||
"build-types": "node scripts/build-types.js",
|
||||
"prebuild": "npm run build-types",
|
||||
"check-strict-null-errors": "tsc --noEmit --strictNullChecks | node scripts/count-strict-null-check-errors.js",
|
||||
"check-types": "tsc --noEmit",
|
||||
|
|
110
scripts/build-types.js
Normal file
110
scripts/build-types.js
Normal file
|
@ -0,0 +1,110 @@
|
|||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const { generate } = require("@graphql-codegen/cli");
|
||||
|
||||
// Feature flags names that will be used as suffix for generated files
|
||||
const FEATURE_FLAGS = [];
|
||||
|
||||
const schemaSuffixes = ["default", ...FEATURE_FLAGS];
|
||||
|
||||
for (const rawSuffix of schemaSuffixes) {
|
||||
const suffix = prepareSuffix(rawSuffix);
|
||||
|
||||
generate(
|
||||
{
|
||||
schema: `./introspection${suffix}.json`,
|
||||
overwrite: true,
|
||||
generates: {
|
||||
[getOutputPath("fragmentTypes", suffix)]: {
|
||||
plugins: [
|
||||
{
|
||||
add: {
|
||||
content: "/* eslint-disable */",
|
||||
},
|
||||
},
|
||||
"fragment-matcher",
|
||||
],
|
||||
config: {
|
||||
minify: false,
|
||||
apolloClientVersion: 3,
|
||||
},
|
||||
},
|
||||
[getOutputPath("typePolicies", suffix)]: {
|
||||
plugins: [
|
||||
{
|
||||
add: {
|
||||
content: "/* eslint-disable */",
|
||||
},
|
||||
},
|
||||
"typescript-apollo-client-helpers",
|
||||
],
|
||||
},
|
||||
[getOutputPath("types", suffix)]: {
|
||||
documents: getDocumentsPaths(suffix),
|
||||
config: {
|
||||
nonOptionalTypename: true,
|
||||
avoidOptionals: {
|
||||
field: true,
|
||||
inputValue: false,
|
||||
object: false,
|
||||
defaultValue: false,
|
||||
},
|
||||
namingConvention: {
|
||||
enumValues: "change-case-all#upperCase",
|
||||
},
|
||||
onlyOperationTypes: true,
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
add: {
|
||||
content: "/* eslint-disable */",
|
||||
},
|
||||
},
|
||||
"typescript",
|
||||
"typescript-operations",
|
||||
],
|
||||
},
|
||||
[getOutputPath("hooks", suffix)]: {
|
||||
documents: getDocumentsPaths(suffix),
|
||||
preset: "import-types",
|
||||
presetConfig: {
|
||||
typesPath: "./types" + suffix + ".generated",
|
||||
},
|
||||
config: {
|
||||
withHooks: true,
|
||||
apolloReactHooksImportFrom: "@dashboard/hooks/graphql",
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
add: {
|
||||
content: "/* eslint-disable */",
|
||||
},
|
||||
},
|
||||
"typescript-react-apollo",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
function getOutputPath(path, suffix) {
|
||||
return `./src/graphql/${path}${suffix}.generated.ts`;
|
||||
}
|
||||
|
||||
function getDocumentsPaths(suffix) {
|
||||
return [
|
||||
`./src/**/queries${suffix}.ts`,
|
||||
`./src/**/mutations${suffix}.ts`,
|
||||
`./src/**/fragments/*${suffix}.ts`,
|
||||
`./src/searches/*${suffix}.ts`,
|
||||
];
|
||||
}
|
||||
|
||||
function prepareSuffix(suffix) {
|
||||
if (suffix === "default") {
|
||||
return "";
|
||||
}
|
||||
|
||||
return "." + suffix;
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
import {
|
||||
AppDetailsUrlQueryParams,
|
||||
getAppDeepPathFromDashboardUrl,
|
||||
prepareFeatureFlagsList,
|
||||
resolveAppIframeUrl,
|
||||
} from "@dashboard/apps/urls";
|
||||
import { useAllFlags } from "@dashboard/hooks/useFlags";
|
||||
import useLocale from "@dashboard/hooks/useLocale";
|
||||
import useShop from "@dashboard/hooks/useShop";
|
||||
import { useTheme } from "@saleor/macaw-ui";
|
||||
|
@ -42,6 +44,7 @@ export const AppFrame: React.FC<Props> = ({
|
|||
const { themeType } = useTheme();
|
||||
const classes = useStyles();
|
||||
const appOrigin = getOrigin(src);
|
||||
const flags = useAllFlags();
|
||||
const { postToExtension } = useAppActions(frameRef, appOrigin, appId);
|
||||
const location = useLocation();
|
||||
const { locale } = useLocale();
|
||||
|
@ -102,7 +105,10 @@ export const AppFrame: React.FC<Props> = ({
|
|||
return (
|
||||
<iframe
|
||||
ref={frameRef}
|
||||
src={resolveAppIframeUrl(appId, src, params)}
|
||||
src={resolveAppIframeUrl(appId, src, {
|
||||
...params,
|
||||
featureFlags: prepareFeatureFlagsList(flags),
|
||||
})}
|
||||
onError={onError}
|
||||
onLoad={handleLoad}
|
||||
className={clsx(classes.iframe, className)}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { getApiUrl } from "@dashboard/config";
|
||||
import { FlagWithName } from "@dashboard/hooks/useFlags/types";
|
||||
import { stringifyQs } from "@dashboard/utils/urls";
|
||||
import urlJoin from "url-join";
|
||||
|
||||
|
@ -26,9 +27,14 @@ export interface AppDetailsUrlMountQueryParams {
|
|||
customerIds?: string[];
|
||||
}
|
||||
|
||||
interface FeatureFlagsQueryParams {
|
||||
featureFlags?: Record<string, string>;
|
||||
}
|
||||
|
||||
export type AppDetailsUrlQueryParams = Dialog<AppDetailsUrlDialog> &
|
||||
SingleAction &
|
||||
AppDetailsUrlMountQueryParams;
|
||||
AppDetailsUrlMountQueryParams &
|
||||
FeatureFlagsQueryParams;
|
||||
|
||||
export type AppInstallUrlQueryParams = Partial<{ [MANIFEST_ATTR]: string }>;
|
||||
|
||||
|
@ -130,3 +136,13 @@ export const resolveAppIframeUrl = (
|
|||
|
||||
return urlJoin(appUrl, window.location.search, iframeContextQueryString);
|
||||
};
|
||||
|
||||
export const prepareFeatureFlagsList = (
|
||||
flags: FlagWithName[],
|
||||
): Record<string, string> =>
|
||||
flags.reduce<Record<string, string>>((acc, flag) => {
|
||||
if (flag.enabled) {
|
||||
acc[flag.name] = `${flag.value || true}`;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
|
1
src/hooks/useFlags/env/const.ts
vendored
Normal file
1
src/hooks/useFlags/env/const.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
export const ENV_FLAG_PREFIX = "FF_";
|
32
src/hooks/useFlags/env/helpers.ts
vendored
Normal file
32
src/hooks/useFlags/env/helpers.ts
vendored
Normal file
|
@ -0,0 +1,32 @@
|
|||
import camelCase from "lodash/camelCase";
|
||||
import snakeCase from "lodash/snakeCase";
|
||||
|
||||
import { ENV_FLAG_PREFIX } from "./const";
|
||||
|
||||
export const envNameToFlagName = (envName: string) => {
|
||||
const name = envName.split(ENV_FLAG_PREFIX)[1];
|
||||
return camelCase(name);
|
||||
};
|
||||
|
||||
export const flagNameToEnvName = (flagName: string) =>
|
||||
`${ENV_FLAG_PREFIX}${snakeCase(flagName).toUpperCase()}`;
|
||||
|
||||
/**
|
||||
Referencing an virtual constant FLAGS, prepared by Vite. It populates env-based feature flags into client-side, under the virtual property FLAGS,
|
||||
Please do not use FLAGS constant directly anywhere.
|
||||
*/
|
||||
export const readFlagFromEnv = (flagName: string): string | undefined => {
|
||||
if (FLAGS) {
|
||||
return FLAGS[flagName];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const readAllFlagsFromEnv = (): Record<string, string> => {
|
||||
if (FLAGS) {
|
||||
return FLAGS;
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
2
src/hooks/useFlags/env/index.ts
vendored
Normal file
2
src/hooks/useFlags/env/index.ts
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { useEnvFlags } from "./useEnvFlags";
|
||||
export { useAllEnvFlags } from "./useAllEnvFlags";
|
40
src/hooks/useFlags/env/useAllEnvFlags.test.tsx
vendored
Normal file
40
src/hooks/useFlags/env/useAllEnvFlags.test.tsx
vendored
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { renderHook } from "@testing-library/react-hooks";
|
||||
|
||||
import { useAllEnvFlags } from "./useAllEnvFlags";
|
||||
|
||||
describe("useAllEnvFlags hook", () => {
|
||||
afterEach(() => {
|
||||
delete FLAGS.FF_FLAG_ONE;
|
||||
delete FLAGS.FF_FLAG_TWO;
|
||||
});
|
||||
|
||||
test("should return all environment flags", () => {
|
||||
// Arrange && Act
|
||||
FLAGS.FF_FLAG_ONE = "1";
|
||||
FLAGS.FF_FLAG_TWO = "2";
|
||||
|
||||
const { result } = renderHook(() => useAllEnvFlags());
|
||||
|
||||
// Assert
|
||||
expect(result.current).toEqual([
|
||||
{
|
||||
name: "flagOne",
|
||||
enabled: true,
|
||||
value: "1",
|
||||
},
|
||||
{
|
||||
name: "flagTwo",
|
||||
enabled: true,
|
||||
value: "2",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("should return empty array when there is no flags", () => {
|
||||
// Arrange && Act
|
||||
const { result } = renderHook(() => useAllEnvFlags());
|
||||
|
||||
// Assert
|
||||
expect(result.current).toEqual([]);
|
||||
});
|
||||
});
|
12
src/hooks/useFlags/env/useAllEnvFlags.ts
vendored
Normal file
12
src/hooks/useFlags/env/useAllEnvFlags.ts
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { FlagWithName } from "../types";
|
||||
import { envNameToFlagName, readAllFlagsFromEnv } from "./helpers";
|
||||
|
||||
export const useAllEnvFlags = (): FlagWithName[] => {
|
||||
const flags = readAllFlagsFromEnv();
|
||||
|
||||
return Object.entries(flags).map(([flagKey, flagValue]) => ({
|
||||
name: envNameToFlagName(flagKey),
|
||||
enabled: flagValue !== "",
|
||||
value: flagValue,
|
||||
}));
|
||||
};
|
57
src/hooks/useFlags/env/useEnvFlags.test.tsx
vendored
Normal file
57
src/hooks/useFlags/env/useEnvFlags.test.tsx
vendored
Normal file
|
@ -0,0 +1,57 @@
|
|||
import { renderHook } from "@testing-library/react-hooks";
|
||||
|
||||
import { useEnvFlags } from "./useEnvFlags";
|
||||
|
||||
describe("useEnvFlags hook", () => {
|
||||
afterEach(() => {
|
||||
delete FLAGS.FF_FLAG_ONE;
|
||||
delete FLAGS.FF_FLAG_TWO;
|
||||
});
|
||||
|
||||
test("should return results for given flags when exists in process.env", () => {
|
||||
// Arrange && Act
|
||||
FLAGS.FF_FLAG_ONE = "1";
|
||||
FLAGS.FF_FLAG_TWO = "2";
|
||||
|
||||
const { result } = renderHook(() => useEnvFlags(["flagOne", "flag_two"]));
|
||||
|
||||
// Assert
|
||||
expect(result.current).toEqual({
|
||||
flagOne: {
|
||||
enabled: true,
|
||||
value: "1",
|
||||
},
|
||||
flag_two: {
|
||||
enabled: true,
|
||||
value: "2",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("should return results for given flags even when flag does not exist", () => {
|
||||
// Arrange && Act
|
||||
|
||||
const { result } = renderHook(() => useEnvFlags(["flagOne", "flag_two"]));
|
||||
|
||||
// Assert
|
||||
expect(result.current).toEqual({
|
||||
flagOne: {
|
||||
enabled: false,
|
||||
value: "",
|
||||
},
|
||||
flag_two: {
|
||||
enabled: false,
|
||||
value: "",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("should return empty object when not flags provided", () => {
|
||||
// Arrange && Act
|
||||
|
||||
const { result } = renderHook(() => useEnvFlags([]));
|
||||
|
||||
// Assert
|
||||
expect(result.current).toEqual({});
|
||||
});
|
||||
});
|
23
src/hooks/useFlags/env/useEnvFlags.ts
vendored
Normal file
23
src/hooks/useFlags/env/useEnvFlags.ts
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { FlagsResults } from "../types";
|
||||
import { flagNameToEnvName, readFlagFromEnv } from "./helpers";
|
||||
|
||||
export const useEnvFlags = <T extends readonly string[]>(
|
||||
flags: readonly [...T],
|
||||
): FlagsResults<T> =>
|
||||
flags.reduce((acc, flag) => {
|
||||
const envFlag = readFlagFromEnv(flagNameToEnvName(flag));
|
||||
|
||||
if (envFlag) {
|
||||
acc[flag] = {
|
||||
enabled: envFlag !== "",
|
||||
value: envFlag,
|
||||
};
|
||||
} else {
|
||||
acc[flag] = {
|
||||
enabled: false,
|
||||
value: "",
|
||||
};
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {} as FlagsResults<T>);
|
27
src/hooks/useFlags/flagsService/flagsServiceProvider.tsx
Normal file
27
src/hooks/useFlags/flagsService/flagsServiceProvider.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import flagsmith from "flagsmith";
|
||||
import { FlagsmithProvider as Provider } from "flagsmith/react";
|
||||
import React from "react";
|
||||
|
||||
interface FlagsServiceProviderProps {
|
||||
children: React.ReactElement;
|
||||
}
|
||||
|
||||
export const FlagsServiceProvider = ({
|
||||
children,
|
||||
}: FlagsServiceProviderProps) => {
|
||||
if (!FLAGS_SERVICE_ENABLED) {
|
||||
return children;
|
||||
}
|
||||
|
||||
return (
|
||||
<Provider
|
||||
flagsmith={flagsmith}
|
||||
options={{
|
||||
environmentID: FLAGSMITH_ID,
|
||||
cacheFlags: true,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Provider>
|
||||
);
|
||||
};
|
3
src/hooks/useFlags/flagsService/index.ts
Normal file
3
src/hooks/useFlags/flagsService/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export { useServiceFlags } from "./useServiceFlags";
|
||||
export { useAllServiceFlags } from "./useAllServiceFlags";
|
||||
export { FlagsServiceProvider } from "./flagsServiceProvider";
|
44
src/hooks/useFlags/flagsService/useAllServiceFlags.test.tsx
Normal file
44
src/hooks/useFlags/flagsService/useAllServiceFlags.test.tsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { renderHook } from "@testing-library/react-hooks";
|
||||
|
||||
import { useAllServiceFlags } from "./useAllServiceFlags";
|
||||
|
||||
jest.mock("flagsmith/react", () => ({
|
||||
__esModule: true,
|
||||
useFlagsmith: () => ({
|
||||
getAllFlags: () => ({
|
||||
flag_one: {
|
||||
enabled: true,
|
||||
value: "1",
|
||||
},
|
||||
flag_two: {
|
||||
enabled: true,
|
||||
value: "2",
|
||||
},
|
||||
}),
|
||||
}),
|
||||
}));
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("useAllServiceFlags hook", () => {
|
||||
test("should return all flags from flag service", () => {
|
||||
// Arrange && Act
|
||||
const { result } = renderHook(() => useAllServiceFlags());
|
||||
|
||||
// Assert
|
||||
expect(result.current).toEqual([
|
||||
{
|
||||
name: "flagOne",
|
||||
enabled: true,
|
||||
value: "1",
|
||||
},
|
||||
{
|
||||
name: "flagTwo",
|
||||
enabled: true,
|
||||
value: "2",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
15
src/hooks/useFlags/flagsService/useAllServiceFlags.ts
Normal file
15
src/hooks/useFlags/flagsService/useAllServiceFlags.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { useFlagsmith } from "flagsmith/react";
|
||||
import camelCase from "lodash/camelCase";
|
||||
|
||||
import { FlagWithName } from "../types";
|
||||
|
||||
export const useAllServiceFlags = (): FlagWithName[] => {
|
||||
const flagsmith = useFlagsmith();
|
||||
const flags = flagsmith.getAllFlags();
|
||||
|
||||
return Object.entries(flags).map(([name, { value, enabled }]) => ({
|
||||
name: camelCase(name),
|
||||
value: value ?? "",
|
||||
enabled,
|
||||
}));
|
||||
};
|
50
src/hooks/useFlags/flagsService/useServiceFlags.test.tsx
Normal file
50
src/hooks/useFlags/flagsService/useServiceFlags.test.tsx
Normal file
|
@ -0,0 +1,50 @@
|
|||
import { renderHook } from "@testing-library/react-hooks";
|
||||
|
||||
import { useServiceFlags } from "./useServiceFlags";
|
||||
|
||||
jest.mock("flagsmith/react", () => ({
|
||||
__esModule: true,
|
||||
useFlags: () => ({
|
||||
flag_one: {
|
||||
enabled: true,
|
||||
value: "1",
|
||||
},
|
||||
flag_two: {
|
||||
enabled: true,
|
||||
value: "2",
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("useServiceFlags", () => {
|
||||
test("should return flags with values", () => {
|
||||
// Arrange && Ac
|
||||
const { result } = renderHook(() =>
|
||||
useServiceFlags(["flagOne", "flag_two"]),
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(result.current).toEqual({
|
||||
flagOne: {
|
||||
enabled: true,
|
||||
value: "1",
|
||||
},
|
||||
flag_two: {
|
||||
enabled: true,
|
||||
value: "2",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("should return empty object when not flags provided", () => {
|
||||
// Arrange && Act
|
||||
const { result } = renderHook(() => useServiceFlags([]));
|
||||
|
||||
// Assert
|
||||
expect(result.current).toEqual({});
|
||||
});
|
||||
});
|
27
src/hooks/useFlags/flagsService/useServiceFlags.ts
Normal file
27
src/hooks/useFlags/flagsService/useServiceFlags.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { useFlags } from "flagsmith/react";
|
||||
import snakeCase from "lodash/snakeCase";
|
||||
|
||||
import { FlagsResults } from "../types";
|
||||
|
||||
export const useServiceFlags = <T extends readonly string[]>(
|
||||
flags: readonly [...T],
|
||||
traits?: string[],
|
||||
): FlagsResults<T> => {
|
||||
const flagsmithFlags = useFlags(transformFlagsToSnakeCase(flags), traits);
|
||||
|
||||
return flags.reduce((acc, flag) => {
|
||||
const flagName = snakeCase(flag);
|
||||
if (flagsmithFlags[flagName]) {
|
||||
acc[flag] = {
|
||||
enabled: flagsmithFlags[flagName].enabled,
|
||||
value: flagsmithFlags[flagName].value ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {} as FlagsResults<T>);
|
||||
};
|
||||
|
||||
function transformFlagsToSnakeCase(flags: readonly string[]): string[] {
|
||||
return flags.map(flag => snakeCase(flag));
|
||||
}
|
7
src/hooks/useFlags/index.ts
Normal file
7
src/hooks/useFlags/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { useAllEnvFlags, useEnvFlags } from "./env";
|
||||
import { useAllServiceFlags, useServiceFlags } from "./flagsService";
|
||||
|
||||
export const useFlags = FLAGS_SERVICE_ENABLED ? useServiceFlags : useEnvFlags;
|
||||
export const useAllFlags = FLAGS_SERVICE_ENABLED
|
||||
? useAllServiceFlags
|
||||
: useAllEnvFlags;
|
12
src/hooks/useFlags/types.ts
Normal file
12
src/hooks/useFlags/types.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
export interface Flag {
|
||||
value: string | number | boolean;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface FlagWithName extends Flag {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type FlagsResults<T extends readonly string[]> = {
|
||||
[Key in T[number]]: Flag;
|
||||
};
|
|
@ -50,6 +50,7 @@ import GiftCardSection from "./giftCards";
|
|||
import { giftCardsSectionUrlName } from "./giftCards/urls";
|
||||
import { apolloClient, saleorClient } from "./graphql/client";
|
||||
import HomePage from "./home";
|
||||
import { FlagsServiceProvider } from "./hooks/useFlags/flagsService";
|
||||
import { commonMessages } from "./intl";
|
||||
import MarketplaceSection from "./marketplace";
|
||||
import { marketplaceUrl } from "./marketplace/urls";
|
||||
|
@ -92,6 +93,7 @@ const App: React.FC = () => (
|
|||
<ServiceWorker />
|
||||
<BackgroundTasksProvider>
|
||||
<AppStateProvider>
|
||||
<FlagsServiceProvider>
|
||||
<AuthProvider>
|
||||
<ShopProvider>
|
||||
<AppChannelProvider>
|
||||
|
@ -103,6 +105,7 @@ const App: React.FC = () => (
|
|||
</AppChannelProvider>
|
||||
</ShopProvider>
|
||||
</AuthProvider>
|
||||
</FlagsServiceProvider>
|
||||
</AppStateProvider>
|
||||
</BackgroundTasksProvider>
|
||||
</MessageManagerProvider>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { ExternalAppProvider } from "@dashboard/apps/components/ExternalAppContext";
|
||||
import { Locale, RawLocaleProvider } from "@dashboard/components/Locale";
|
||||
import { FlagsServiceProvider } from "@dashboard/hooks/useFlags/flagsService";
|
||||
import themeOverrides from "@dashboard/themeOverrides";
|
||||
import { ThemeProvider } from "@saleor/macaw-ui";
|
||||
import React from "react";
|
||||
|
@ -26,6 +27,7 @@ export const Decorator = storyFn => (
|
|||
<ThemeProvider overrides={themeOverrides}>
|
||||
<BrowserRouter basename={getAppMountUri()}>
|
||||
<ExternalAppProvider>
|
||||
<FlagsServiceProvider>
|
||||
<MessageManagerProvider>
|
||||
<div
|
||||
style={{
|
||||
|
@ -35,6 +37,7 @@ export const Decorator = storyFn => (
|
|||
{storyFn()}
|
||||
</div>
|
||||
</MessageManagerProvider>
|
||||
</FlagsServiceProvider>
|
||||
</ExternalAppProvider>
|
||||
</BrowserRouter>
|
||||
</ThemeProvider>
|
||||
|
|
4
types.d.ts
vendored
4
types.d.ts
vendored
|
@ -5,6 +5,10 @@ declare module "*.svg" {
|
|||
export default content;
|
||||
}
|
||||
|
||||
declare const FLAGS_SERVICE_ENABLED: boolean;
|
||||
declare const FLAGSMITH_ID: string;
|
||||
declare const FLAGS: Record<string, string>;
|
||||
|
||||
declare interface Window {
|
||||
PasswordCredential: PasswordCredential;
|
||||
__SALEOR_CONFIG__: {
|
||||
|
|
|
@ -35,8 +35,14 @@ export default defineConfig(({ command, mode }) => {
|
|||
APPS_TUNNEL_URL_KEYWORDS,
|
||||
SKIP_SOURCEMAPS,
|
||||
DEMO_MODE,
|
||||
FLAGS_SERVICE_ENABLED,
|
||||
FLAGSMITH_ID,
|
||||
} = env;
|
||||
|
||||
const featureFlagsEnvs = Object.fromEntries(
|
||||
Object.entries(env).filter(([flagKey]) => flagKey.startsWith("FF_")),
|
||||
);
|
||||
|
||||
const sourcemap = SKIP_SOURCEMAPS ? false : true;
|
||||
|
||||
const enableSentry =
|
||||
|
@ -96,11 +102,17 @@ export default defineConfig(({ command, mode }) => {
|
|||
);
|
||||
}
|
||||
|
||||
const globals = {
|
||||
/*
|
||||
"qs" package uses 'get-intrinsic' whish refers to the global object, we need to recreate it.
|
||||
Issue presents only on development mode.
|
||||
*/
|
||||
const globals = isDev ? { global: {} } : {};
|
||||
...(isDev ? { global: {} } : {}),
|
||||
FLAGS_SERVICE_ENABLED: FLAGS_SERVICE_ENABLED === "true",
|
||||
FLAGSMITH_ID: JSON.stringify(FLAGSMITH_ID),
|
||||
// Keep all feature flags from env in global variable
|
||||
FLAGS: JSON.stringify(featureFlagsEnvs),
|
||||
};
|
||||
|
||||
return {
|
||||
root: "src",
|
||||
|
|
Loading…
Reference in a new issue