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 }) => {
|
.then(({ variantsList: variants }) => {
|
||||||
variantsList = variants
|
variantsList = variants;
|
||||||
cy.checkIfDataAreNotNull({createdApp,defaultChannel,address,warehouse,shippingMethod,variantsList,checkout})
|
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 }) => {
|
.then(({ product: productResp }) => {
|
||||||
product = productResp
|
product = productResp;
|
||||||
cy.checkIfDataAreNotNull({attribute, category, productType, product, defaultChannel})
|
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 }) => {
|
.then(({ variantsList: variantsResp }) => {
|
||||||
variants = 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 }) => {
|
.then(({ variantsList }) => {
|
||||||
variantsInOtherWarehouse = 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,
|
name: nameProdWithoutShipping,
|
||||||
productTypeId: productTypeResp.id,
|
productTypeId: productTypeResp.id,
|
||||||
warehouseId: warehouse.id,
|
warehouseId: warehouse.id,
|
||||||
}).then(
|
}).then(({ variantsList }) => {
|
||||||
({ variantsList }) => {
|
productWithoutShipping = variantsList;
|
||||||
productWithoutShipping = variantsList
|
cy.checkIfDataAreNotNull({
|
||||||
cy.checkIfDataAreNotNull({channel, address, warehouse, shippingMethod, productWithShipping, productWithoutShipping})
|
channel,
|
||||||
}
|
address,
|
||||||
);
|
warehouse,
|
||||||
|
shippingMethod,
|
||||||
|
productWithShipping,
|
||||||
|
productWithoutShipping,
|
||||||
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -56,7 +56,14 @@ describe("Manage products stocks in checkout", () => {
|
||||||
lastVariantInStock = resp.createdVariants.find(
|
lastVariantInStock = resp.createdVariants.find(
|
||||||
variant => variant.name === "lastVariantInStock",
|
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));
|
getDefaultChannel().then(defaultChannel => (channel = defaultChannel));
|
||||||
createCategory({ name: startsWith }).then(
|
createCategory({ name: startsWith }).then(
|
||||||
categoryResp => (category = categoryResp),
|
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 => {
|
}).then(attributeResp => {
|
||||||
attribute = attributeResp;
|
attribute = attributeResp;
|
||||||
});
|
});
|
||||||
cy.checkIfDataAreNotNull(attribute)
|
cy.checkIfDataAreNotNull(attribute);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(
|
it(
|
||||||
|
|
|
@ -24,12 +24,10 @@ describe("Tests for menu navigation", () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.clearSessionData().loginUserViaRequest();
|
cy.clearSessionData().loginUserViaRequest();
|
||||||
deleteMenusStartsWith(startsWith);
|
deleteMenusStartsWith(startsWith);
|
||||||
createMenuViaApi(randomName).then(
|
createMenuViaApi(randomName).then(({ menu: menuResp }) => {
|
||||||
({ menu: menuResp }) => {
|
menu = menuResp;
|
||||||
menu = menuResp
|
cy.checkIfDataAreNotNull(menu);
|
||||||
cy.checkIfDataAreNotNull(menu)
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
|
@ -92,8 +92,17 @@ describe("Adyen payments", () => {
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(({ variantsList: variants }) => {
|
.then(({ variantsList: variants }) => {
|
||||||
variantsList = variants
|
variantsList = variants;
|
||||||
cy.checkIfDataAreNotNull({address,defaultChannel,warehouse,shippingMethod,variantsList,checkout,paymentCards,cardData})
|
cy.checkIfDataAreNotNull({
|
||||||
|
address,
|
||||||
|
defaultChannel,
|
||||||
|
warehouse,
|
||||||
|
shippingMethod,
|
||||||
|
variantsList,
|
||||||
|
checkout,
|
||||||
|
paymentCards,
|
||||||
|
cardData,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -50,7 +50,15 @@ describe("Stripe payments", () => {
|
||||||
defaultChannel = values.defaultChannel;
|
defaultChannel = values.defaultChannel;
|
||||||
shippingMethod = values.shippingMethod;
|
shippingMethod = values.shippingMethod;
|
||||||
variantsList = values.variantsList;
|
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));
|
createAttribute({ name: startsWith }).then(resp => (attribute = resp));
|
||||||
createCategory({ name: startsWith }).then(resp => (category = resp));
|
createCategory({ name: startsWith }).then(resp => (category = resp));
|
||||||
getDefaultChannel().then(resp => {
|
getDefaultChannel().then(resp => {
|
||||||
channel = resp
|
channel = resp;
|
||||||
cy.checkIfDataAreNotNull({attribute, channel, category})
|
cy.checkIfDataAreNotNull({ attribute, channel, category });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -95,7 +95,14 @@ describe("As a staff user I want to create shipping zone and rate", () => {
|
||||||
})
|
})
|
||||||
.then(variantsListResp => {
|
.then(variantsListResp => {
|
||||||
secondVariantsList = 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(
|
createShippingZone(name, "US", defaultChannel.id, warehouse.id).then(
|
||||||
shippingZoneResp => {
|
shippingZoneResp => {
|
||||||
shippingZone = 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;
|
warehouse = warehouseResp;
|
||||||
|
|
||||||
updateChannelWarehouses(defaultChannel.id, warehouse.id);
|
updateChannelWarehouses(defaultChannel.id, warehouse.id);
|
||||||
cy.checkIfDataAreNotNull({defaultChannel, shippingZone, plAddress, warehouse})
|
cy.checkIfDataAreNotNull({
|
||||||
|
defaultChannel,
|
||||||
|
shippingZone,
|
||||||
|
plAddress,
|
||||||
|
warehouse,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
||||||
cy.clearSessionData().loginUserViaRequest();
|
cy.clearSessionData().loginUserViaRequest();
|
||||||
createShippingZone(name, "US", defaultChannel.id, warehouse.id).then(
|
createShippingZone(name, "US", defaultChannel.id, warehouse.id).then(
|
||||||
shippingZoneResp => {
|
shippingZoneResp => {
|
||||||
|
|
|
@ -83,7 +83,14 @@ describe("As a user I want to create shipping method with postal codes", () => {
|
||||||
})
|
})
|
||||||
.then(({ variantsList: variantsListResp }) => {
|
.then(({ variantsList: variantsListResp }) => {
|
||||||
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 }) => {
|
.then(({ variantsList: variantsListResp }) => {
|
||||||
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 => {
|
.then(shippingZoneResp => {
|
||||||
shippingZone = 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 }) => {
|
cy.fixture("addresses").then(({ usAddress, plAddress }) => {
|
||||||
address = usAddress;
|
address = usAddress;
|
||||||
updateShopAddress(plAddress);
|
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 => {
|
cy.fixture("addresses").then(addresses => {
|
||||||
usAddress = addresses.usAddress;
|
usAddress = addresses.usAddress;
|
||||||
secondUsAddress = addresses.secondUsAddress;
|
secondUsAddress = addresses.secondUsAddress;
|
||||||
cy.checkIfDataAreNotNull({usAddress,secondUsAddress})
|
cy.checkIfDataAreNotNull({ usAddress, secondUsAddress });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ describe("Tests for customer registration", () => {
|
||||||
deleteCustomersStartsWith(startsWith);
|
deleteCustomersStartsWith(startsWith);
|
||||||
getDefaultChannel().then(channel => {
|
getDefaultChannel().then(channel => {
|
||||||
defaultChannel = 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 }) => {
|
.then(({ warehouse: warehouseResp }) => {
|
||||||
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,
|
warehouseId: warehouse.id,
|
||||||
price: productPrice,
|
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,
|
warehouseId: warehouse.id,
|
||||||
price: productPrice,
|
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",
|
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,
|
shippingMethodName: shippingMethodResp.name,
|
||||||
auth: "token",
|
auth: "token",
|
||||||
};
|
};
|
||||||
cy.checkIfDataAreNotNull({dataForCheckout, defaultChannel})
|
cy.checkIfDataAreNotNull({ dataForCheckout, defaultChannel });
|
||||||
cy.clearSessionData();
|
cy.clearSessionData();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -51,7 +51,7 @@ describe("As an admin I want to update vouchers", () => {
|
||||||
auth: "token",
|
auth: "token",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
cy.checkIfDataAreNotNull({dataForCheckout, defaultChannel, product})
|
cy.checkIfDataAreNotNull({ dataForCheckout, defaultChannel, product }),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,7 @@ describe("Test for metadata", () => {
|
||||||
})
|
})
|
||||||
.then(({ product: productResp }) => {
|
.then(({ product: productResp }) => {
|
||||||
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;
|
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 }) => {
|
.then(({ variantsList: variantsResp }) => {
|
||||||
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();
|
cy.clearSessionData().loginUserViaRequest();
|
||||||
createAttribute({ name }).then(attributeResp => {
|
createAttribute({ name }).then(attributeResp => {
|
||||||
attribute = attributeResp;
|
attribute = attributeResp;
|
||||||
cy.checkIfDataAreNotNull({attribute})
|
cy.checkIfDataAreNotNull({ attribute });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
|
@ -27,7 +27,7 @@ describe("As an admin I should be able to sort products", () => {
|
||||||
cy.clearSessionData().loginUserViaRequest();
|
cy.clearSessionData().loginUserViaRequest();
|
||||||
getDefaultChannel().then(channel => {
|
getDefaultChannel().then(channel => {
|
||||||
defaultChannel = 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" });
|
createChannel({ isActive: true, name, currencyCode: "PLN" });
|
||||||
})
|
})
|
||||||
.then(resp => {
|
.then(resp => {
|
||||||
newChannel = resp
|
newChannel = resp;
|
||||||
cy.checkIfDataAreNotNull({defaultChannel,warehouse,attribute,productType,category,newChannel})
|
cy.checkIfDataAreNotNull({
|
||||||
|
defaultChannel,
|
||||||
|
warehouse,
|
||||||
|
attribute,
|
||||||
|
productType,
|
||||||
|
category,
|
||||||
|
newChannel,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,7 @@ describe("Staff members", () => {
|
||||||
cy.clearSessionData().visit(urlLink);
|
cy.clearSessionData().visit(urlLink);
|
||||||
fillUpSetPassword(password);
|
fillUpSetPassword(password);
|
||||||
cy.clearSessionData();
|
cy.clearSessionData();
|
||||||
cy.checkIfDataAreNotNull({user})
|
cy.checkIfDataAreNotNull({ user });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -20,12 +20,10 @@ describe("As an admin I want to manage translations", () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.clearSessionData().loginUserViaRequest();
|
cy.clearSessionData().loginUserViaRequest();
|
||||||
deleteCategoriesStartsWith(startsWith);
|
deleteCategoriesStartsWith(startsWith);
|
||||||
createCategory({ name: startsWith }).then(
|
createCategory({ name: startsWith }).then(categoryResp => {
|
||||||
categoryResp => {
|
category = categoryResp;
|
||||||
category = categoryResp
|
cy.checkIfDataAreNotNull({ category });
|
||||||
cy.checkIfDataAreNotNull({category})
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
Cypress.Commands.add("getTextFromElement", element =>
|
Cypress.Commands.add("getTextFromElement", element =>
|
||||||
cy.get(element).invoke("text")
|
cy.get(element).invoke("text"),
|
||||||
);
|
);
|
||||||
|
|
||||||
Cypress.Commands.add("clearAndType", { prevSubject: true }, (subject, text) => {
|
Cypress.Commands.add("clearAndType", { prevSubject: true }, (subject, text) => {
|
||||||
|
@ -12,22 +12,22 @@ Cypress.Commands.add("waitForRequestAndCheckIfNoErrors", alias => {
|
||||||
cy.wait(alias).then(resp => {
|
cy.wait(alias).then(resp => {
|
||||||
expect(
|
expect(
|
||||||
resp.response.body.errors,
|
resp.response.body.errors,
|
||||||
`No errors in ${alias} operation in graphql response`
|
`No errors in ${alias} operation in graphql response`,
|
||||||
).to.be.undefined;
|
).to.be.undefined;
|
||||||
return resp;
|
return resp;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add("checkIfDataAreNotNull", (data) => {
|
Cypress.Commands.add("checkIfDataAreNotNull", data => {
|
||||||
expect(data, "Created data should not be null").to.be.not.null;
|
expect(data, "Created data should not be null").to.be.not.null;
|
||||||
if(typeof data === "object"){
|
if (typeof data === "object") {
|
||||||
Object.keys(data).forEach(key => {
|
Object.keys(data).forEach(key => {
|
||||||
cy.checkIfDataAreNotNull(data[key])
|
cy.checkIfDataAreNotNull(data[key]);
|
||||||
})
|
});
|
||||||
}else if(Array.isArray(data)){
|
} else if (Array.isArray(data)) {
|
||||||
expect(data).not.to.be.empty;
|
expect(data).not.to.be.empty;
|
||||||
data.forEach(singleData => {
|
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_MARKETPLACE_API_URI` - URI of Marketplace API to fetch list of Apps in JSON.
|
||||||
|
|
||||||
- `APPS_TUNNEL_URL_KEYWORDS` - Custom apps tunnel URL keywords.
|
- `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",
|
"faker": "^5.1.0",
|
||||||
"fast-array-diff": "^0.2.0",
|
"fast-array-diff": "^0.2.0",
|
||||||
"find-test-names": "^1.17.1",
|
"find-test-names": "^1.17.1",
|
||||||
|
"flagsmith": "^3.15.1",
|
||||||
"fuse.js": "^6.6.2",
|
"fuse.js": "^6.6.2",
|
||||||
"fuzzaldrin": "^2.1.0",
|
"fuzzaldrin": "^2.1.0",
|
||||||
"graphql": "^15.4.0",
|
"graphql": "^15.4.0",
|
||||||
|
@ -21046,6 +21047,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/first-match/-/first-match-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/first-match/-/first-match-0.0.1.tgz",
|
||||||
"integrity": "sha1-pg7GQnAPD0NyNOu37D84JHblQv0="
|
"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": {
|
"node_modules/flat": {
|
||||||
"version": "5.0.2",
|
"version": "5.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/first-match/-/first-match-0.0.1.tgz",
|
||||||
"integrity": "sha1-pg7GQnAPD0NyNOu37D84JHblQv0="
|
"integrity": "sha1-pg7GQnAPD0NyNOu37D84JHblQv0="
|
||||||
},
|
},
|
||||||
|
"flagsmith": {
|
||||||
|
"version": "3.15.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/flagsmith/-/flagsmith-3.15.1.tgz",
|
||||||
|
"integrity": "sha512-4OHVCCYuW1doYceEJbrQWJgefwRaC9oi2VeswVzCqCe0WCoT0pOIxgi14l3Am0a0fcN9wUd93Dv2OQaF6GqKjA=="
|
||||||
|
},
|
||||||
"flat": {
|
"flat": {
|
||||||
"version": "5.0.2",
|
"version": "5.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
|
||||||
|
|
|
@ -47,6 +47,7 @@
|
||||||
"faker": "^5.1.0",
|
"faker": "^5.1.0",
|
||||||
"fast-array-diff": "^0.2.0",
|
"fast-array-diff": "^0.2.0",
|
||||||
"find-test-names": "^1.17.1",
|
"find-test-names": "^1.17.1",
|
||||||
|
"flagsmith": "^3.15.1",
|
||||||
"fuse.js": "^6.6.2",
|
"fuse.js": "^6.6.2",
|
||||||
"fuzzaldrin": "^2.1.0",
|
"fuzzaldrin": "^2.1.0",
|
||||||
"graphql": "^15.4.0",
|
"graphql": "^15.4.0",
|
||||||
|
@ -212,6 +213,10 @@
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"resetMocks": false,
|
"resetMocks": false,
|
||||||
|
"globals": {
|
||||||
|
"FLAGS_SERVICE_ENABLED": false,
|
||||||
|
"FLAGS": {}
|
||||||
|
},
|
||||||
"setupFilesAfterEnv": [
|
"setupFilesAfterEnv": [
|
||||||
"jest-canvas-mock",
|
"jest-canvas-mock",
|
||||||
"jest-localstorage-mock",
|
"jest-localstorage-mock",
|
||||||
|
@ -262,7 +267,7 @@
|
||||||
"build": "cross-env NODE_OPTIONS=--max_old_space_size=16384 vite build",
|
"build": "cross-env NODE_OPTIONS=--max_old_space_size=16384 vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"build-storybook": "cross-env NODE_OPTIONS=--openssl-legacy-provider build-storybook -c src/storybook/ -o build/storybook",
|
"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",
|
"prebuild": "npm run build-types",
|
||||||
"check-strict-null-errors": "tsc --noEmit --strictNullChecks | node scripts/count-strict-null-check-errors.js",
|
"check-strict-null-errors": "tsc --noEmit --strictNullChecks | node scripts/count-strict-null-check-errors.js",
|
||||||
"check-types": "tsc --noEmit",
|
"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 {
|
import {
|
||||||
AppDetailsUrlQueryParams,
|
AppDetailsUrlQueryParams,
|
||||||
getAppDeepPathFromDashboardUrl,
|
getAppDeepPathFromDashboardUrl,
|
||||||
|
prepareFeatureFlagsList,
|
||||||
resolveAppIframeUrl,
|
resolveAppIframeUrl,
|
||||||
} from "@dashboard/apps/urls";
|
} from "@dashboard/apps/urls";
|
||||||
|
import { useAllFlags } from "@dashboard/hooks/useFlags";
|
||||||
import useLocale from "@dashboard/hooks/useLocale";
|
import useLocale from "@dashboard/hooks/useLocale";
|
||||||
import useShop from "@dashboard/hooks/useShop";
|
import useShop from "@dashboard/hooks/useShop";
|
||||||
import { useTheme } from "@saleor/macaw-ui";
|
import { useTheme } from "@saleor/macaw-ui";
|
||||||
|
@ -42,6 +44,7 @@ export const AppFrame: React.FC<Props> = ({
|
||||||
const { themeType } = useTheme();
|
const { themeType } = useTheme();
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const appOrigin = getOrigin(src);
|
const appOrigin = getOrigin(src);
|
||||||
|
const flags = useAllFlags();
|
||||||
const { postToExtension } = useAppActions(frameRef, appOrigin, appId);
|
const { postToExtension } = useAppActions(frameRef, appOrigin, appId);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { locale } = useLocale();
|
const { locale } = useLocale();
|
||||||
|
@ -102,7 +105,10 @@ export const AppFrame: React.FC<Props> = ({
|
||||||
return (
|
return (
|
||||||
<iframe
|
<iframe
|
||||||
ref={frameRef}
|
ref={frameRef}
|
||||||
src={resolveAppIframeUrl(appId, src, params)}
|
src={resolveAppIframeUrl(appId, src, {
|
||||||
|
...params,
|
||||||
|
featureFlags: prepareFeatureFlagsList(flags),
|
||||||
|
})}
|
||||||
onError={onError}
|
onError={onError}
|
||||||
onLoad={handleLoad}
|
onLoad={handleLoad}
|
||||||
className={clsx(classes.iframe, className)}
|
className={clsx(classes.iframe, className)}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { getApiUrl } from "@dashboard/config";
|
import { getApiUrl } from "@dashboard/config";
|
||||||
|
import { FlagWithName } from "@dashboard/hooks/useFlags/types";
|
||||||
import { stringifyQs } from "@dashboard/utils/urls";
|
import { stringifyQs } from "@dashboard/utils/urls";
|
||||||
import urlJoin from "url-join";
|
import urlJoin from "url-join";
|
||||||
|
|
||||||
|
@ -26,9 +27,14 @@ export interface AppDetailsUrlMountQueryParams {
|
||||||
customerIds?: string[];
|
customerIds?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FeatureFlagsQueryParams {
|
||||||
|
featureFlags?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
export type AppDetailsUrlQueryParams = Dialog<AppDetailsUrlDialog> &
|
export type AppDetailsUrlQueryParams = Dialog<AppDetailsUrlDialog> &
|
||||||
SingleAction &
|
SingleAction &
|
||||||
AppDetailsUrlMountQueryParams;
|
AppDetailsUrlMountQueryParams &
|
||||||
|
FeatureFlagsQueryParams;
|
||||||
|
|
||||||
export type AppInstallUrlQueryParams = Partial<{ [MANIFEST_ATTR]: string }>;
|
export type AppInstallUrlQueryParams = Partial<{ [MANIFEST_ATTR]: string }>;
|
||||||
|
|
||||||
|
@ -130,3 +136,13 @@ export const resolveAppIframeUrl = (
|
||||||
|
|
||||||
return urlJoin(appUrl, window.location.search, iframeContextQueryString);
|
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 { giftCardsSectionUrlName } from "./giftCards/urls";
|
||||||
import { apolloClient, saleorClient } from "./graphql/client";
|
import { apolloClient, saleorClient } from "./graphql/client";
|
||||||
import HomePage from "./home";
|
import HomePage from "./home";
|
||||||
|
import { FlagsServiceProvider } from "./hooks/useFlags/flagsService";
|
||||||
import { commonMessages } from "./intl";
|
import { commonMessages } from "./intl";
|
||||||
import MarketplaceSection from "./marketplace";
|
import MarketplaceSection from "./marketplace";
|
||||||
import { marketplaceUrl } from "./marketplace/urls";
|
import { marketplaceUrl } from "./marketplace/urls";
|
||||||
|
@ -92,17 +93,19 @@ const App: React.FC = () => (
|
||||||
<ServiceWorker />
|
<ServiceWorker />
|
||||||
<BackgroundTasksProvider>
|
<BackgroundTasksProvider>
|
||||||
<AppStateProvider>
|
<AppStateProvider>
|
||||||
<AuthProvider>
|
<FlagsServiceProvider>
|
||||||
<ShopProvider>
|
<AuthProvider>
|
||||||
<AppChannelProvider>
|
<ShopProvider>
|
||||||
<ExternalAppProvider>
|
<AppChannelProvider>
|
||||||
<ExitFormDialogProvider>
|
<ExternalAppProvider>
|
||||||
<Routes />
|
<ExitFormDialogProvider>
|
||||||
</ExitFormDialogProvider>
|
<Routes />
|
||||||
</ExternalAppProvider>
|
</ExitFormDialogProvider>
|
||||||
</AppChannelProvider>
|
</ExternalAppProvider>
|
||||||
</ShopProvider>
|
</AppChannelProvider>
|
||||||
</AuthProvider>
|
</ShopProvider>
|
||||||
|
</AuthProvider>
|
||||||
|
</FlagsServiceProvider>
|
||||||
</AppStateProvider>
|
</AppStateProvider>
|
||||||
</BackgroundTasksProvider>
|
</BackgroundTasksProvider>
|
||||||
</MessageManagerProvider>
|
</MessageManagerProvider>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { ExternalAppProvider } from "@dashboard/apps/components/ExternalAppContext";
|
import { ExternalAppProvider } from "@dashboard/apps/components/ExternalAppContext";
|
||||||
import { Locale, RawLocaleProvider } from "@dashboard/components/Locale";
|
import { Locale, RawLocaleProvider } from "@dashboard/components/Locale";
|
||||||
|
import { FlagsServiceProvider } from "@dashboard/hooks/useFlags/flagsService";
|
||||||
import themeOverrides from "@dashboard/themeOverrides";
|
import themeOverrides from "@dashboard/themeOverrides";
|
||||||
import { ThemeProvider } from "@saleor/macaw-ui";
|
import { ThemeProvider } from "@saleor/macaw-ui";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
@ -26,15 +27,17 @@ export const Decorator = storyFn => (
|
||||||
<ThemeProvider overrides={themeOverrides}>
|
<ThemeProvider overrides={themeOverrides}>
|
||||||
<BrowserRouter basename={getAppMountUri()}>
|
<BrowserRouter basename={getAppMountUri()}>
|
||||||
<ExternalAppProvider>
|
<ExternalAppProvider>
|
||||||
<MessageManagerProvider>
|
<FlagsServiceProvider>
|
||||||
<div
|
<MessageManagerProvider>
|
||||||
style={{
|
<div
|
||||||
padding: 24,
|
style={{
|
||||||
}}
|
padding: 24,
|
||||||
>
|
}}
|
||||||
{storyFn()}
|
>
|
||||||
</div>
|
{storyFn()}
|
||||||
</MessageManagerProvider>
|
</div>
|
||||||
|
</MessageManagerProvider>
|
||||||
|
</FlagsServiceProvider>
|
||||||
</ExternalAppProvider>
|
</ExternalAppProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
|
4
types.d.ts
vendored
4
types.d.ts
vendored
|
@ -5,6 +5,10 @@ declare module "*.svg" {
|
||||||
export default content;
|
export default content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare const FLAGS_SERVICE_ENABLED: boolean;
|
||||||
|
declare const FLAGSMITH_ID: string;
|
||||||
|
declare const FLAGS: Record<string, string>;
|
||||||
|
|
||||||
declare interface Window {
|
declare interface Window {
|
||||||
PasswordCredential: PasswordCredential;
|
PasswordCredential: PasswordCredential;
|
||||||
__SALEOR_CONFIG__: {
|
__SALEOR_CONFIG__: {
|
||||||
|
|
|
@ -35,8 +35,14 @@ export default defineConfig(({ command, mode }) => {
|
||||||
APPS_TUNNEL_URL_KEYWORDS,
|
APPS_TUNNEL_URL_KEYWORDS,
|
||||||
SKIP_SOURCEMAPS,
|
SKIP_SOURCEMAPS,
|
||||||
DEMO_MODE,
|
DEMO_MODE,
|
||||||
|
FLAGS_SERVICE_ENABLED,
|
||||||
|
FLAGSMITH_ID,
|
||||||
} = env;
|
} = env;
|
||||||
|
|
||||||
|
const featureFlagsEnvs = Object.fromEntries(
|
||||||
|
Object.entries(env).filter(([flagKey]) => flagKey.startsWith("FF_")),
|
||||||
|
);
|
||||||
|
|
||||||
const sourcemap = SKIP_SOURCEMAPS ? false : true;
|
const sourcemap = SKIP_SOURCEMAPS ? false : true;
|
||||||
|
|
||||||
const enableSentry =
|
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.
|
"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 {
|
return {
|
||||||
root: "src",
|
root: "src",
|
||||||
|
|
Loading…
Reference in a new issue